├── Archipelago.HollowKnight ├── ModDependencies.txt ├── Resources │ ├── IconBlue.png │ ├── IconColor.png │ ├── GrubHappyv2.png │ ├── IconBlueBig.png │ ├── Pins │ │ ├── pinAP.png │ │ ├── pinAPUseful.png │ │ └── pinAPProgression.png │ ├── DeathLinkIcon.png │ ├── IconBlueSmall.png │ ├── IconColorBig.png │ ├── IconColorSmall.png │ └── Data │ │ └── starts.json ├── GeneratedConsts.cs ├── IC │ ├── ArchipelagoSprite.cs │ ├── Items │ │ └── GoalItem.cs │ ├── PlacementUtils.cs │ ├── Modules │ │ ├── DupeHandlingModule.cs │ │ ├── RepositionShadeModule.cs │ │ ├── GoalModule.cs │ │ ├── ArchipelagoRemoteItemCounterModule.cs │ │ ├── BenchSyncModule.cs │ │ ├── GiftingModule.cs │ │ ├── ItemNetworkingModule.cs │ │ └── DeathLinkModule.cs │ ├── RM │ │ ├── NOTICE.md │ │ ├── StartDef.cs │ │ ├── StartLocationSceneEditsModule.cs │ │ └── HelperPlatformBuilder.cs │ ├── RemotePlacement.cs │ ├── Tags │ │ └── ArchipelagoRemoteItemTag.cs │ ├── ArchipelagoUIDef.cs │ ├── ItemFactory.cs │ ├── ArchipelagoItem.cs │ ├── CostFactory.cs │ └── ArchipelagoTags.cs ├── Extensions.cs ├── Enums.cs ├── LoginValidationException.cs ├── Settings.cs ├── SlotDataModel │ ├── SlotData.cs │ └── SlotOptions.cs ├── DupeUIDef.cs ├── TimeoutExtensions.cs ├── DeathLinkMessages.cs ├── HintTracker.cs ├── MC │ └── ArchipelagoModeMenuConstructor.cs ├── ArchipelagoMod.cs ├── Goals.cs ├── ArchipelagoRandomizer.cs └── Archipelago.HollowKnight.csproj ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── auto-dependabot.yml │ └── build-release.yml ├── LICENSE.txt ├── Archipelago.HollowKnight.sln ├── .gitattributes ├── README.md └── .gitignore /Archipelago.HollowKnight/ModDependencies.txt: -------------------------------------------------------------------------------- 1 | ItemChanger 2 | MenuChanger 3 | Benchwarp -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/IconBlue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight/HEAD/Archipelago.HollowKnight/Resources/IconBlue.png -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/IconColor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight/HEAD/Archipelago.HollowKnight/Resources/IconColor.png -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/GrubHappyv2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight/HEAD/Archipelago.HollowKnight/Resources/GrubHappyv2.png -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/IconBlueBig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight/HEAD/Archipelago.HollowKnight/Resources/IconBlueBig.png -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/Pins/pinAP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight/HEAD/Archipelago.HollowKnight/Resources/Pins/pinAP.png -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/DeathLinkIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight/HEAD/Archipelago.HollowKnight/Resources/DeathLinkIcon.png -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/IconBlueSmall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight/HEAD/Archipelago.HollowKnight/Resources/IconBlueSmall.png -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/IconColorBig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight/HEAD/Archipelago.HollowKnight/Resources/IconColorBig.png -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/IconColorSmall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight/HEAD/Archipelago.HollowKnight/Resources/IconColorSmall.png -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/Pins/pinAPUseful.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight/HEAD/Archipelago.HollowKnight/Resources/Pins/pinAPUseful.png -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/Pins/pinAPProgression.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight/HEAD/Archipelago.HollowKnight/Resources/Pins/pinAPProgression.png -------------------------------------------------------------------------------- /Archipelago.HollowKnight/GeneratedConsts.cs: -------------------------------------------------------------------------------- 1 | using DataDrivenConstants.Marker; 2 | 3 | namespace Archipelago.HollowKnight; 4 | 5 | [JsonData("$.*~", "**/Data/starts.json")] 6 | [ReplacementRule("'", "")] 7 | public static partial class StartLocationNames { } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - directory: / 4 | package-ecosystem: nuget 5 | schedule: 6 | interval: daily 7 | - directory: / 8 | package-ecosystem: github-actions 9 | schedule: 10 | interval: daily -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/ArchipelagoSprite.cs: -------------------------------------------------------------------------------- 1 | using ItemChanger; 2 | using ItemChanger.Internal; 3 | 4 | namespace Archipelago.HollowKnight.IC 5 | { 6 | public class ArchipelagoSprite : EmbeddedSprite 7 | { 8 | public override SpriteManager SpriteManager => ArchipelagoMod.Instance.spriteManager; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Extensions.cs: -------------------------------------------------------------------------------- 1 | using ItemChanger; 2 | 3 | namespace Archipelago.HollowKnight; 4 | 5 | public static class Extensions 6 | { 7 | public static string GetPreviewWithCost(this AbstractItem item) 8 | { 9 | string text = item.GetPreviewName(); 10 | if (item.GetTag(out CostTag tag)) 11 | { 12 | text += " - " + tag.Cost.GetCostText(); 13 | } 14 | return text; 15 | } 16 | } -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace Archipelago.HollowKnight 2 | { 3 | public enum DeathLinkStatus 4 | { 5 | None = 0, 6 | Pending = 1, 7 | Dying = 2 8 | } 9 | 10 | public enum DeathLinkShadeHandling 11 | { 12 | Vanilla = 0, 13 | Shadeless = 1, 14 | Shade = 2 15 | } 16 | 17 | public enum WhitePalaceOption 18 | { 19 | Exclude = 0, 20 | KingFragment = 1, 21 | NoPathOfPain = 2, 22 | Include = 3 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/LoginValidationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Archipelago.HollowKnight 4 | { 5 | internal class LoginValidationException : Exception 6 | { 7 | public LoginValidationException() 8 | { 9 | } 10 | 11 | public LoginValidationException(string message) : base(message) 12 | { 13 | } 14 | 15 | public LoginValidationException(string message, Exception innerException) : base(message, innerException) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/Items/GoalItem.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.HollowKnight.IC.Modules; 2 | using ItemChanger; 3 | 4 | namespace Archipelago.HollowKnight.IC.Items 5 | { 6 | public class GoalItem : AbstractItem 7 | { 8 | private GoalModule goalModule; 9 | 10 | protected override void OnLoad() 11 | { 12 | goalModule = ItemChangerMod.Modules.Get(); 13 | } 14 | 15 | public override async void GiveImmediate(GiveInfo info) 16 | { 17 | await goalModule.DeclareVictoryAsync(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/PlacementUtils.cs: -------------------------------------------------------------------------------- 1 | using ItemChanger; 2 | using System.Collections.Generic; 3 | 4 | namespace Archipelago.HollowKnight.IC 5 | { 6 | internal static class PlacementUtils 7 | { 8 | internal static IEnumerable GetLocationIDs(AbstractPlacement pmt) 9 | { 10 | ArchipelagoItemTag tag; 11 | foreach (AbstractItem item in pmt.Items) 12 | { 13 | tag = item.GetTag(); 14 | if (tag != null) 15 | { 16 | yield return tag.Location; 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, needs investigation 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement, needs investigation 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Settings.cs: -------------------------------------------------------------------------------- 1 | namespace Archipelago.HollowKnight 2 | { 3 | public record ConnectionDetails 4 | { 5 | public string ServerUrl { get; set; } = "archipelago.gg"; 6 | public int ServerPort { get; set; } = 38281; 7 | public string SlotName { get; set; } 8 | public string ServerPassword { get; set; } 9 | } 10 | 11 | public record APGlobalSettings 12 | { 13 | public ConnectionDetails MenuConnectionDetails { get; set; } = new(); 14 | public bool EnableGifting { get; set; } = true; 15 | } 16 | 17 | public record APLocalSettings 18 | { 19 | public ConnectionDetails ConnectionDetails { get; set; } 20 | public string RoomSeed { get; set; } 21 | public long Seed { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/Modules/DupeHandlingModule.cs: -------------------------------------------------------------------------------- 1 | using ItemChanger; 2 | using ItemChanger.Items; 3 | using ItemChanger.Modules; 4 | 5 | namespace Archipelago.HollowKnight.IC.Modules; 6 | 7 | public class DupeHandlingModule : Module 8 | { 9 | public override void Initialize() 10 | { 11 | AbstractItem.ModifyRedundantItemGlobal += ModifyRedundantItem; 12 | } 13 | 14 | public override void Unload() 15 | { 16 | AbstractItem.ModifyRedundantItemGlobal -= ModifyRedundantItem; 17 | } 18 | 19 | private void ModifyRedundantItem(GiveEventArgs args) 20 | { 21 | args.Item = new SpawnLumafliesItem 22 | { 23 | name = $"Lumafly_Escape-{args.Orig.name}", 24 | UIDef = DupeUIDef.Of(args.Orig.UIDef) 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/SlotDataModel/SlotData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | 4 | namespace Archipelago.HollowKnight.SlotDataModel 5 | { 6 | public class SlotData 7 | { 8 | [JsonProperty("seed")] 9 | public int Seed { get; set; } 10 | 11 | [JsonProperty("options")] 12 | public SlotOptions Options { get; set; } 13 | 14 | [JsonProperty("location_costs")] 15 | public Dictionary> LocationCosts { get; set; } 16 | 17 | [JsonProperty("notch_costs")] 18 | public List NotchCosts { get; set; } 19 | 20 | [JsonProperty("grub_count")] 21 | public int? GrubsRequired { get; set; } 22 | 23 | [JsonProperty("is_race")] 24 | public bool DisableLocalSpoilerLogs { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/RM/NOTICE.md: -------------------------------------------------------------------------------- 1 | The files in this directory are taken with permission from [https://github.com/homothetyhk/RandomizerMod](RandomizerMod) under the LGPL license (also present in this directory). 2 | 3 | ## Changes made 4 | 5 | ### HelperPlatformBuilder.cs 6 | 7 | * Moved and renamed from RandomizerMod/IC/PlatformList.cs 8 | * Added start-dependent helper platforms from RandomizerMod/IC/Export.cs's ExportStart method 9 | * Changed namespaces and imports according to new file structure 10 | * Changed arguments of `GetPlatformList` to use Archipelago's settings objects rather than RandomizerMod's 11 | 12 | 13 | ### StartLocationSceneEditsModule 14 | 15 | * Moved and renamed from RandomizerMod/IC/RandomizerModule.cs 16 | * Changed namespaces and imports according to new file structure 17 | * Scoped to only handle scene edits to prevent starting softlocks 18 | * Adapted to use Archipelago's settings rather than RandomizerMod's 19 | * Formatted to match this project's formatting style 20 | -------------------------------------------------------------------------------- /.github/workflows/auto-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: github.actor == 'dependabot[bot]' 12 | steps: 13 | - name: Dependabot metadata 14 | uses: dependabot/fetch-metadata@v2 15 | with: 16 | github-token: ${{ secrets.GITHUB_TOKEN }} 17 | - name: Approve PR 18 | if: steps.metadata.outputs.update-type != 'version-update:semver-major' 19 | run: gh pr review --approve "$PR_URL" 20 | env: 21 | PR_URL: ${{ github.event.pull_request.html_url }} 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | - name: Auto merge 24 | if: steps.metadata.outputs.update-type != 'version-update:semver-major' 25 | run: gh pr merge --rebase --auto "$PR_URL" 26 | env: 27 | PR_URL: ${{ github.event.pull_request.html_url }} 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hussein Farran 4 | Copyright (c) 2022 Daniel Grace 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/RemotePlacement.cs: -------------------------------------------------------------------------------- 1 | using ItemChanger; 2 | using ItemChanger.Extensions; 3 | using ItemChanger.Internal; 4 | using ItemChanger.Tags; 5 | using Newtonsoft.Json; 6 | 7 | namespace Archipelago.HollowKnight.IC 8 | { 9 | public class RemotePlacement : AbstractPlacement 10 | { 11 | public const string SINGLETON_NAME = "Remote_Items"; 12 | 13 | [JsonConstructor] 14 | private RemotePlacement(string Name) : base(SINGLETON_NAME) { } 15 | 16 | public static RemotePlacement GetOrAddSingleton() 17 | { 18 | if (!Ref.Settings.Placements.TryGetValue(SINGLETON_NAME, out AbstractPlacement pmt)) 19 | { 20 | pmt = new RemotePlacement(SINGLETON_NAME); 21 | CompletionWeightTag remoteCompletionWeightTag = pmt.AddTag(); 22 | remoteCompletionWeightTag.Weight = 0; 23 | InteropTag pinTag = new() 24 | { 25 | Message = "RandoSupplementalMetadata", 26 | Properties = new() 27 | { 28 | ["DoNotMakePin"] = true, 29 | } 30 | }; 31 | pmt.AddTag(pinTag); 32 | ItemChangerMod.AddPlacements(pmt.Yield()); 33 | } 34 | return (RemotePlacement)pmt; 35 | } 36 | 37 | protected override void OnLoad() 38 | { 39 | 40 | } 41 | 42 | protected override void OnUnload() 43 | { 44 | 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/Modules/RepositionShadeModule.cs: -------------------------------------------------------------------------------- 1 | using ItemChanger.Modules; 2 | using Modding; 3 | using System.Collections.Generic; 4 | 5 | namespace Archipelago.HollowKnight.IC.Modules 6 | { 7 | public class RepositionShadeModule : Module 8 | { 9 | private static readonly Dictionary ShadeSpawnPositionFixes = new() 10 | { 11 | { "Abyss_08", (90.0f, 90.0f) }, // Lifeblood Core room. Even outside of deathlink, shades spawn out of bounds. 12 | { "Room_Colosseum_Spectate", (124.0f, 10.0f) }, // Shade spawns inside inaccessible arena 13 | { "Resting_Grounds_09", (7.4f, 10.0f) }, // Shade spawns underground. 14 | { "Runes1_18", (11.5f, 23.0f) }, // Shade potentially spawns on the wrong side of an inaccessible gate. 15 | }; 16 | 17 | public override void Initialize() 18 | { 19 | ModHooks.AfterPlayerDeadHook += FixUnreachableShadePosition; 20 | } 21 | 22 | public override void Unload() 23 | { 24 | ModHooks.AfterPlayerDeadHook -= FixUnreachableShadePosition; 25 | } 26 | private void FixUnreachableShadePosition() 27 | { 28 | // Fixes up some bad shade placements by vanilla HK. 29 | PlayerData pd = PlayerData.instance; 30 | if (ShadeSpawnPositionFixes.TryGetValue(pd.shadeScene, out (float x, float y) position)) 31 | { 32 | pd.shadePositionX = position.x; 33 | pd.shadePositionY = position.y; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32228.430 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Archipelago.HollowKnight", "Archipelago.HollowKnight\Archipelago.HollowKnight.csproj", "{7B597421-1AA1-4880-B095-7A293B7FF39E}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EA5CCD5F-7520-420B-B06F-5506BB4E18F6}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitattributes = .gitattributes 11 | .gitignore = .gitignore 12 | .github\workflows\auto-dependabot.yml = .github\workflows\auto-dependabot.yml 13 | .github\workflows\build-release.yml = .github\workflows\build-release.yml 14 | .github\dependabot.yml = .github\dependabot.yml 15 | README.md = README.md 16 | EndProjectSection 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {7B597421-1AA1-4880-B095-7A293B7FF39E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {7B597421-1AA1-4880-B095-7A293B7FF39E}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {7B597421-1AA1-4880-B095-7A293B7FF39E}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {7B597421-1AA1-4880-B095-7A293B7FF39E}.Release|Any CPU.Build.0 = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(SolutionProperties) = preSolution 30 | HideSolutionNode = FALSE 31 | EndGlobalSection 32 | GlobalSection(ExtensibilityGlobals) = postSolution 33 | SolutionGuid = {3054058D-AA08-4F84-AFE6-6665F9B3BBA2} 34 | EndGlobalSection 35 | EndGlobal 36 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/DupeUIDef.cs: -------------------------------------------------------------------------------- 1 | using ItemChanger; 2 | using ItemChanger.UIDefs; 3 | using UnityEngine; 4 | 5 | namespace Archipelago.HollowKnight; 6 | 7 | public class DupeUIDef : MsgUIDef 8 | { 9 | public static MsgUIDef Of(UIDef inner) 10 | { 11 | if (inner is MsgUIDef msg) 12 | { 13 | return new SplitUIDef 14 | { 15 | preview = new BoxedString(msg.GetPreviewName()), 16 | name = new BoxedString($"Nothing ({msg.GetPostviewName()})"), 17 | shopDesc = msg.shopDesc?.Clone(), 18 | sprite = msg.sprite?.Clone(), 19 | }; 20 | } 21 | return new DupeUIDef(inner); 22 | } 23 | 24 | public UIDef Inner { get; set; } 25 | private DupeUIDef(UIDef inner) 26 | { 27 | Inner = inner; 28 | sprite = new ItemChangerSprite("ShopIcons.LampBug"); 29 | if (inner is null) 30 | { 31 | name = new BoxedString("Nothing (Dupe)"); 32 | shopDesc = new BoxedString(""); 33 | } 34 | else 35 | { 36 | // with good practice these should never be accessed but better not to break stuff 37 | name = new BoxedString($"Nothing ({inner.GetPostviewName()})"); 38 | shopDesc = new BoxedString(inner.GetShopDesc()); 39 | } 40 | } 41 | 42 | public override Sprite GetSprite() => Inner is not null ? Inner.GetSprite() : base.GetSprite(); 43 | public override string GetPreviewName() => Inner is not null ? Inner.GetPreviewName() : base.GetPreviewName(); 44 | public override string GetPostviewName() => Inner is not null ? Inner.GetPostviewName() : base.GetPostviewName(); 45 | public override string GetShopDesc() => Inner is not null ? Inner.GetShopDesc() : base.GetShopDesc(); 46 | } 47 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/Tags/ArchipelagoRemoteItemTag.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.HollowKnight.IC.Modules; 2 | using Archipelago.MultiClient.Net.Models; 3 | using ItemChanger; 4 | using Newtonsoft.Json; 5 | using System; 6 | 7 | namespace Archipelago.HollowKnight.IC.Tags; 8 | 9 | 10 | /// 11 | /// Tag attached to items sent from other players 12 | /// 13 | public class ArchipelagoRemoteItemTag : Tag 14 | { 15 | /// 16 | /// The slot ID of the sending player 17 | /// 18 | public int Sender { get; set; } 19 | 20 | /// 21 | /// The location ID in the sender's world 22 | /// 23 | public long LocationId { get; set; } 24 | 25 | /// 26 | /// The item ID 27 | /// 28 | public long ItemId { get; set; } 29 | 30 | [JsonConstructor] 31 | private ArchipelagoRemoteItemTag() { } 32 | 33 | public ArchipelagoRemoteItemTag(ItemInfo itemInfo) 34 | { 35 | if (itemInfo is ScoutedItemInfo) 36 | { 37 | throw new ArgumentException("Remote item tags should only be used on items received from other players and should not be initialized from scouts", nameof(itemInfo)); 38 | } 39 | ArchipelagoMod.Instance.LogDebug($"Created remote tag for {itemInfo.ItemName} from {itemInfo.Player} at {itemInfo.LocationDisplayName}"); 40 | Sender = itemInfo.Player; 41 | LocationId = itemInfo.LocationId; 42 | ItemId = itemInfo.ItemId; 43 | } 44 | 45 | public override void Load(object parent) 46 | { 47 | base.Load(parent); 48 | ArchipelagoRemoteItemCounterModule module = ItemChangerMod.Modules.GetOrAdd(); 49 | module.IncrementSavedCountForItem(Sender, LocationId, ItemId); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/RM/StartDef.cs: -------------------------------------------------------------------------------- 1 | using GlobalEnums; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | 7 | namespace Archipelago.HollowKnight.IC.RM; 8 | public record StartDef 9 | { 10 | public static Dictionary Lookup 11 | { 12 | get 13 | { 14 | if (field != null) 15 | { 16 | return field; 17 | } 18 | JsonSerializer ser = new() 19 | { 20 | TypeNameHandling = TypeNameHandling.Auto, 21 | Converters = 22 | { 23 | new StringEnumConverter() 24 | } 25 | }; 26 | using StreamReader r = new(typeof(StartDef).Assembly.GetManifestResourceStream("Archipelago.HollowKnight.Resources.Data.starts.json")); 27 | using JsonTextReader reader = new(r); 28 | return field = ser.Deserialize>(reader); 29 | } 30 | } 31 | 32 | public string Name { get; init; } 33 | public string SceneName { get; init; } 34 | public float X { get; init; } 35 | public float Y { get; init; } 36 | public MapZone Zone { get; init; } 37 | /// 38 | /// Granted transition in logic 39 | /// 40 | public string Transition { get; init; } 41 | 42 | public ItemChanger.StartDef ToItemChangerStartDef() 43 | { 44 | return new ItemChanger.StartDef 45 | { 46 | SceneName = SceneName, 47 | X = X, 48 | Y = Y, 49 | MapZone = (int)Zone, 50 | RespawnFacingRight = true, 51 | SpecialEffects = ItemChanger.SpecialStartEffects.Default | ItemChanger.SpecialStartEffects.SlowSoulRefill, 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/Modules/GoalModule.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.MultiClient.Net; 2 | using Archipelago.MultiClient.Net.Enums; 3 | using Archipelago.MultiClient.Net.Exceptions; 4 | using Archipelago.MultiClient.Net.Packets; 5 | using ItemChanger; 6 | using ItemChanger.Modules; 7 | using System; 8 | using System.Threading.Tasks; 9 | 10 | namespace Archipelago.HollowKnight.IC.Modules 11 | { 12 | public class GoalModule : Module 13 | { 14 | private ArchipelagoSession session => ArchipelagoMod.Instance.session; 15 | 16 | private Goal goal; 17 | 18 | public bool queuedGoal = false; 19 | 20 | public override void Initialize() 21 | { 22 | goal = Goal.GetGoal(ArchipelagoMod.Instance.SlotData.Options.Goal); 23 | goal.Select(); 24 | Events.OnEnterGame += OnEnterGame; 25 | } 26 | 27 | public override void Unload() 28 | { 29 | Events.OnEnterGame -= OnEnterGame; 30 | goal.Deselect(); 31 | goal = null; 32 | } 33 | 34 | public async Task DeclareVictoryAsync() 35 | { 36 | try 37 | { 38 | await session.Socket.SendPacketAsync(new StatusUpdatePacket() 39 | { 40 | Status = ArchipelagoClientState.ClientGoal 41 | }).TimeoutAfter(1000); 42 | queuedGoal = false; 43 | } 44 | catch (Exception ex) when (ex is TimeoutException or ArchipelagoSocketClosedException) 45 | { 46 | ItemChangerMod.Modules.Get().ReportDisconnect(); 47 | queuedGoal = true; 48 | } 49 | } 50 | 51 | private async void OnEnterGame() 52 | { 53 | if (queuedGoal) 54 | { 55 | await DeclareVictoryAsync(); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/ArchipelagoUIDef.cs: -------------------------------------------------------------------------------- 1 | using ItemChanger; 2 | using ItemChanger.UIDefs; 3 | 4 | namespace Archipelago.HollowKnight.IC 5 | { 6 | internal class ArchipelagoUIDef : MsgUIDef 7 | { 8 | public static string GetSentItemName(AbstractItem item) 9 | { 10 | return item.name switch 11 | { 12 | ItemNames.Grub => "A grub!", 13 | ItemNames.Grimmkin_Flame => "Grimmkin Flame", 14 | ItemNames.Rancid_Egg => "Rancid Egg", 15 | _ => item.UIDef.GetPostviewName(), 16 | }; 17 | } 18 | 19 | public static ArchipelagoUIDef CreateForReceivedItem(AbstractItem item, string sender) 20 | { 21 | return CreateForReceivedItem(item.GetResolvedUIDef(), sender); 22 | } 23 | 24 | public static ArchipelagoUIDef CreateForReceivedItem(UIDef source, string sender) 25 | { 26 | ArchipelagoUIDef result = new(source); 27 | result.name = new BoxedString($"{source.GetPostviewName()} from {sender}"); 28 | return result; 29 | } 30 | public static ArchipelagoUIDef CreateForSentItem(AbstractItem item, string recipient) 31 | { 32 | ArchipelagoUIDef result = new(item.UIDef); 33 | result.name = new BoxedString($"{recipient}'s {GetSentItemName(item)}"); 34 | return result; 35 | } 36 | 37 | internal ArchipelagoUIDef() : base() 38 | { 39 | } 40 | 41 | internal ArchipelagoUIDef(UIDef source) : base() 42 | { 43 | if (source is MsgUIDef msgDef) 44 | { 45 | shopDesc = msgDef.shopDesc.Clone(); 46 | sprite = msgDef.sprite.Clone(); 47 | } 48 | else 49 | { 50 | shopDesc = new BoxedString(source.GetShopDesc()); 51 | sprite = new EmptySprite(); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the master branch 5 | push: 6 | branches: [ "main" ] 7 | tags: [ "v*.*.*" ] 8 | pull_request: 9 | branches: [ "main" ] 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | build: 16 | runs-on: windows-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v6 21 | 22 | - name: Setup MAPI 23 | uses: BadMagic100/setup-hk@v2 24 | with: 25 | apiPath: API 26 | dependencyFilePath: Archipelago.HollowKnight/ModDependencies.txt 27 | 28 | - name: Setup .NET 29 | uses: actions/setup-dotnet@v5 30 | 31 | - name: Install dependencies 32 | run: dotnet restore 33 | 34 | - name: Build 35 | run: dotnet build -c $Env:BUILD_CONFIGURATION 36 | env: 37 | BUILD_CONFIGURATION: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) && 'Release' || 'Debug' }} 38 | 39 | - name: Prepare artifacts for release 40 | uses: actions/upload-artifact@v6 41 | with: 42 | name: Archipelago 43 | path: Archipelago.HollowKnight/bin/Publish 44 | release: 45 | needs: 46 | - build 47 | runs-on: windows-latest 48 | # only make a release if we tagged for it 49 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 50 | steps: 51 | - name: Download Artifacts 52 | uses: actions/download-artifact@v7 53 | with: 54 | name: Archipelago 55 | path: artifacts/Archipelago 56 | - run: Get-ChildItem -Recurse 57 | - name: Get build details 58 | id: details 59 | # this assumes that an MSBuild task that writes the SHA256 of the zip file to SHA.txt, and the mod version (usually 60 | # the same as the assembly version) to version.txt. The contents of these files are read to step outputs for use in release 61 | run: | 62 | $sha = Get-Content artifacts/Archipelago/SHA.txt 63 | echo "archiveHash=$sha" >> $env:GITHUB_OUTPUT 64 | - name: Release 65 | uses: softprops/action-gh-release@v2 66 | with: 67 | draft: false 68 | generate_release_notes: true 69 | fail_on_unmatched_files: true 70 | body: | 71 | SHA256: ${{ steps.details.outputs.archiveHash }} 72 | files: | 73 | artifacts/Archipelago/Archipelago.zip 74 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/ItemFactory.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.MultiClient.Net; 2 | using Archipelago.MultiClient.Net.Models; 3 | using ItemChanger; 4 | using ItemChanger.Items; 5 | using ItemChanger.Tags; 6 | using System; 7 | 8 | namespace Archipelago.HollowKnight.IC 9 | { 10 | internal class ItemFactory 11 | { 12 | public AbstractItem CreateMyItem(string itemName, ScoutedItemInfo itemInfo) 13 | { 14 | AbstractItem item = Finder.GetItem(itemName); 15 | if (item == null) 16 | { 17 | ArchipelagoMod.Instance.LogError($"Could not find local item with name {itemName}"); 18 | throw new NullReferenceException($"Could not find local item with name {itemName}"); 19 | } 20 | 21 | AddArchipelagoTag(item, itemInfo); 22 | return item; 23 | } 24 | 25 | public AbstractItem CreateRemoteItem(AbstractPlacement targetPlacement, string slotName, string itemName, ScoutedItemInfo itemInfo) 26 | { 27 | ArchipelagoSession session = ArchipelagoMod.Instance.session; 28 | string game = itemInfo.ItemGame; 29 | 30 | AbstractItem orig = Finder.GetItem(itemName); 31 | AbstractItem item; 32 | if (game == "Hollow Knight" && orig != null) 33 | { 34 | // this is a remote HK item - make it a no-op, but cosmetically correct 35 | item = new ArchipelagoDummyItem(orig); 36 | item.UIDef = ArchipelagoUIDef.CreateForSentItem(orig, slotName); 37 | 38 | // give the placement the correct cosmetic soul totem or geo rock type if appropriate 39 | if (orig is SoulTotemItem totem) 40 | { 41 | targetPlacement.GetOrAddTag().Type = totem.soulTotemSubtype; 42 | } 43 | else if (orig is GeoRockItem rock) 44 | { 45 | targetPlacement.GetOrAddTag().Type = rock.geoRockSubtype; 46 | } 47 | } 48 | else 49 | { 50 | // Items from other games, or an unknown HK item 51 | item = new ArchipelagoItem(itemName, slotName, itemInfo.Flags); 52 | } 53 | 54 | AddArchipelagoTag(item, itemInfo); 55 | return item; 56 | } 57 | 58 | private void AddArchipelagoTag(AbstractItem item, ScoutedItemInfo itemInfo) 59 | { 60 | ArchipelagoItemTag itemTag = item.AddTag(); 61 | itemTag.ReadItemInfo(itemInfo); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Archipelago.HollowKnight 2 | 3 | A mod which enables Hollow Knight to act as an Archipelago client, enabling multiworld and randomization driven by the [Archipelago multigame multiworld system](https://archipelago.gg). 4 | 5 | ## Installing Archipelago.HollowKnight 6 | ### Installing with Lumafly 7 | 1. [Download Lumafly](https://themulhima.github.io/Lumafly?download). 8 | 2. Place Lumafly in a folder other than your Downloads folder and run it 9 | * If it does not detect your HK install directory, lead Lumafly to the correct directory. 10 | * Also, don’t pirate the game. >:( 11 | 3. Install and enable Archipelago. 12 | * There are several mods that are needed to for Archipelago to run. They are installed automatically. 13 | * Archipelago Map Mod is an in-game tracker for Archipelago. It is optional and can also be installed from Lumafly. 14 | 4. Start the game and ensure **Archipelago** appears in the top left corner of the main menu. 15 | 16 | ## Joining an Archipelago Session 17 | 1. Start the game after installing all necessary mods. 18 | 2. Create a **new save game.** 19 | 3. Select the **Archipelago** game mode from the mode selection screen. 20 | 4. Enter in the correct settings for your Archipelago server. 21 | 5. Hit **Start** to begin the game. The game will stall for a few seconds while it does all item placements. 22 | 6. The game will immediately drop you into the randomized game. So if you are waiting for a countdown then wait for it to lapse before hitting Start, or hit Start then pause the game once you're in it. 23 | 24 | # Contributing 25 | Contributions are welcome, all code is licensed under the MIT License. Please track your work within the repository if you are taking on a feature. This is done via GitHub Issues. If you are interesting in taking on an issue please comment on the issue to have it assigned to you. If you are looking to contribute something that isn't in the issues list then please submit an issue to describe what work you intend to take on. 26 | 27 | Contribution guidelines: 28 | * All issues should be labeled appropriately. 29 | * All in-progress issues should have someone assigned to them. 30 | * Pull Requests must have at least (and preferably exactly) one linked issue which they close out. 31 | * Please use feature branches, especially if working in this repository (not a fork). 32 | * Please match the style of the surrounding code. In particular: 33 | * Don't use `var`. 34 | * Use shorthand constructor syntax in declarations, and only in declarations (for example, `ArchipelagoRandomizer randomizer = new(slotData);`). 35 | * Always enclose the body of control flow statements (`if`, `foreach`, etc.) in braces, even for single-line bodies. 36 | 37 | ## Development Setup 38 | Follow the instructions in the csproj file to create a LocalOverrides.targets file with your Hollow Knight installation path. If you use the Hollow Knight Modding Visual Studio extension (recommended), there is an item template to create this file for you automatically. 39 | 40 | Post-build events will automatically package the mod for export **as well as install it in your HK installation.** When developing on the mod **do not install Archipelago through an installer.** Some installers, e.g. Lumafly, can pin the development version to prevent it from being replaced by the production version from the installer. 41 | 42 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/TimeoutExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Archipelago.HollowKnight 6 | { 7 | // implementation from https://devblogs.microsoft.com/pfxteam/crafting-a-task-timeoutafter-method/ 8 | internal static class TimeoutExtensions 9 | { 10 | private struct VoidTypeStruct { } 11 | 12 | private static void MarshalTaskResults(Task source, TaskCompletionSource proxy) 13 | { 14 | switch (source.Status) 15 | { 16 | case TaskStatus.Faulted: 17 | proxy.TrySetException(source.Exception); 18 | break; 19 | case TaskStatus.Canceled: 20 | proxy.TrySetCanceled(); 21 | break; 22 | case TaskStatus.RanToCompletion: 23 | Task castedSource = source as Task; 24 | proxy.TrySetResult( 25 | castedSource == null ? default : // source is a Task 26 | castedSource.Result); // source is a Task 27 | break; 28 | } 29 | } 30 | 31 | public static Task TimeoutAfter(this Task task, int millisecondsTimeout) 32 | { 33 | // Short-circuit #1: infinite timeout or task already completed 34 | if (task.IsCompleted || (millisecondsTimeout == Timeout.Infinite)) 35 | { 36 | // Either the task has already completed or timeout will never occur. 37 | // No proxy necessary. 38 | return task; 39 | } 40 | 41 | // tcs.Task will be returned as a proxy to the caller 42 | TaskCompletionSource tcs = new(); 43 | 44 | // Short-circuit #2: zero timeout 45 | if (millisecondsTimeout == 0) 46 | { 47 | // We've already timed out. 48 | tcs.SetException(new TimeoutException()); 49 | return tcs.Task; 50 | } 51 | 52 | // Set up a timer to complete after the specified timeout period 53 | Timer timer = new Timer(state => 54 | { 55 | // Recover your state information 56 | var myTcs = (TaskCompletionSource)state; 57 | 58 | // Fault our proxy with a TimeoutException 59 | myTcs.TrySetException(new TimeoutException()); 60 | }, tcs, millisecondsTimeout, Timeout.Infinite); 61 | 62 | // Wire up the logic for what happens when source task completes 63 | task.ContinueWith((antecedent, state) => 64 | { 65 | // Recover our state data 66 | var tuple = 67 | (Tuple>)state; 68 | 69 | // Cancel the Timer 70 | tuple.Item1.Dispose(); 71 | 72 | // Marshal results to proxy 73 | MarshalTaskResults(antecedent, tuple.Item2); 74 | }, 75 | Tuple.Create(timer, tcs), 76 | CancellationToken.None, 77 | TaskContinuationOptions.ExecuteSynchronously, 78 | TaskScheduler.Default); 79 | 80 | return tcs.Task; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/ArchipelagoItem.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.MultiClient.Net.Enums; 2 | using ItemChanger; 3 | using ItemChanger.Tags; 4 | 5 | namespace Archipelago.HollowKnight.IC 6 | { 7 | public class ArchipelagoDummyItem : AbstractItem 8 | { 9 | public string PreferredContainerType { get; set; } = Container.Unknown; 10 | 11 | public override string GetPreferredContainer() => PreferredContainerType; 12 | 13 | public ArchipelagoDummyItem() 14 | { } 15 | public ArchipelagoDummyItem(AbstractItem source) 16 | { 17 | this.name = source.name; 18 | this.UIDef = source.UIDef.Clone(); 19 | PreferredContainerType = source.GetPreferredContainer(); 20 | } 21 | 22 | public override bool GiveEarly(string containerType) 23 | { 24 | // any container (e.g. a grub or soul totem) that would not normally fling a shiny 25 | // in vanilla should not go out of its way to do so for this 26 | return containerType switch 27 | { 28 | Container.Unknown 29 | or Container.Shiny 30 | or Container.Chest 31 | => false, 32 | _ => true 33 | }; 34 | } 35 | 36 | public override void GiveImmediate(GiveInfo info) 37 | { 38 | // Intentional no-op 39 | } 40 | } 41 | 42 | public class ArchipelagoItem : AbstractItem 43 | { 44 | public ArchipelagoItem(string name, string recipientName = null, ItemFlags itemFlags = 0) 45 | { 46 | string desc; 47 | ISprite pinSprite; 48 | if (itemFlags.HasFlag(ItemFlags.Advancement)) 49 | { 50 | desc = "This otherworldly artifact looks very important. Somebody probably really needs it."; 51 | pinSprite = new ArchipelagoSprite { key = "Pins.pinAPProgression" }; 52 | } 53 | else if (itemFlags.HasFlag(ItemFlags.NeverExclude)) 54 | { 55 | desc = "This otherworldly artifact looks like it might be useful to someone."; 56 | pinSprite = new ArchipelagoSprite { key = "Pins.pinAPUseful" }; 57 | } 58 | else 59 | { 60 | desc = "I'm not entirely sure what this is. It appears to be a strange artifact from another world."; 61 | pinSprite = new ArchipelagoSprite { key = "Pins.pinAP" }; 62 | } 63 | if (itemFlags.HasFlag(ItemFlags.Trap)) 64 | { 65 | desc += " Seems kinda suspicious though. It might be full of bees."; 66 | } 67 | this.name = name; 68 | UIDef = new ArchipelagoUIDef() 69 | { 70 | name = new BoxedString($"{recipientName}'s {name}"), 71 | shopDesc = new BoxedString(desc), 72 | sprite = new ArchipelagoSprite { key = "IconColorSmall" } 73 | }; 74 | InteropTag mapInteropTag = new() 75 | { 76 | Message = "RandoSupplementalMetadata", 77 | Properties = new() { 78 | ["PinSprite"] = pinSprite 79 | } 80 | }; 81 | this.AddTag(mapInteropTag); 82 | } 83 | 84 | public override void GiveImmediate(GiveInfo info) 85 | { 86 | // intentional no-op 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/RM/StartLocationSceneEditsModule.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.HollowKnight.SlotDataModel; 2 | using ItemChanger; 3 | using ItemChanger.Extensions; 4 | using ItemChanger.Modules; 5 | using UnityEngine; 6 | using UnityEngine.SceneManagement; 7 | using UObject = UnityEngine.Object; 8 | 9 | namespace Archipelago.HollowKnight.IC.RM; 10 | public class StartLocationSceneEditsModule : Module 11 | { 12 | public override void Initialize() 13 | { 14 | ToggleSceneHooks(true); 15 | } 16 | 17 | public override void Unload() 18 | { 19 | ToggleSceneHooks(false); 20 | } 21 | 22 | private static void ToggleSceneHooks(bool toggle) 23 | { 24 | SlotOptions options = ArchipelagoMod.Instance.SlotData.Options; 25 | string startLocation = options.StartLocationName ?? StartLocationNames.Kings_Pass; 26 | 27 | switch (startLocation) 28 | { 29 | case "Ancestral Mound": 30 | if (options.RandomizeNail) 31 | { 32 | if (toggle) 33 | { 34 | Events.AddSceneChangeEdit(SceneNames.Crossroads_ShamanTemple, DestroyPlanksForAncestralMoundStart); 35 | } 36 | else 37 | { 38 | Events.RemoveSceneChangeEdit(SceneNames.Crossroads_ShamanTemple, DestroyPlanksForAncestralMoundStart); 39 | } 40 | } 41 | break; 42 | 43 | case "Fungal Core": 44 | if (toggle) 45 | { 46 | Events.AddSceneChangeEdit(SceneNames.Fungus2_30, CreateBounceShroomsForFungalCoreStart); 47 | } 48 | else 49 | { 50 | Events.RemoveSceneChangeEdit(SceneNames.Fungus2_30, CreateBounceShroomsForFungalCoreStart); 51 | } 52 | 53 | break; 54 | 55 | case "West Crossroads": 56 | if (toggle) 57 | { 58 | Events.AddSceneChangeEdit(SceneNames.Crossroads_36, MoveShadeMarkerForWestCrossroadsStart); 59 | } 60 | else 61 | { 62 | Events.RemoveSceneChangeEdit(SceneNames.Crossroads_36, MoveShadeMarkerForWestCrossroadsStart); 63 | } 64 | 65 | break; 66 | } 67 | 68 | 69 | } 70 | 71 | // Destroy planks in cursed nail mode because we can't slash them 72 | private static void DestroyPlanksForAncestralMoundStart(Scene to) 73 | { 74 | foreach ((_, GameObject go) in to.Traverse()) 75 | { 76 | if (go.name.StartsWith("Plank")) 77 | { 78 | UObject.Destroy(go); 79 | } 80 | } 81 | } 82 | 83 | private static void CreateBounceShroomsForFungalCoreStart(Scene to) 84 | { 85 | GameObject bounceShroom = to.FindGameObjectByName("Bounce Shroom C"); 86 | 87 | GameObject s0 = UObject.Instantiate(bounceShroom); 88 | s0.transform.SetPosition3D(12.5f, 26f, 0f); 89 | s0.SetActive(true); 90 | 91 | GameObject s1 = UObject.Instantiate(bounceShroom); 92 | s1.transform.SetPosition3D(12.5f, 54f, 0f); 93 | s1.SetActive(true); 94 | 95 | GameObject s2 = UObject.Instantiate(bounceShroom); 96 | s2.transform.SetPosition3D(21.7f, 133f, 0f); 97 | s2.SetActive(true); 98 | } 99 | 100 | private static void MoveShadeMarkerForWestCrossroadsStart(Scene to) 101 | { 102 | GameObject marker = to.FindGameObject("_Props/Hollow_Shade Marker 1"); 103 | marker.transform.position = new(46.2f, 28f); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/Modules/ArchipelagoRemoteItemCounterModule.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.MultiClient.Net.Models; 2 | using ItemChanger.Modules; 3 | using System.Collections.Generic; 4 | 5 | namespace Archipelago.HollowKnight.IC.Modules; 6 | 7 | public class ArchipelagoRemoteItemCounterModule : Module 8 | { 9 | /// 10 | /// Full history of remote items received and saved by the client. Keys are player, location, item to finally reach a count. 11 | /// 12 | private readonly Dictionary>> savedItemCounts = new(); 13 | 14 | /// 15 | /// History of items seen when receiving from server. Keys are player, location, item to finally reach a count. 16 | /// 17 | 18 | private readonly Dictionary>> serverSeenItemCounts = new(); 19 | 20 | public override void Initialize() 21 | { 22 | } 23 | 24 | public override void Unload() 25 | { 26 | } 27 | 28 | /// 29 | /// Determines whether the specified item should be received from the server based on the current counts. Should be called before receiving the item. 30 | /// 31 | /// The item to evaluate for receiving from the server. 32 | /// 33 | /// if receiving the item would result in the server count exceeding the local saved count; 34 | /// otherwise, . 35 | /// 36 | public bool ShouldReceiveServerItem(ItemInfo item) 37 | { 38 | int currentSavedCount = EnsureCountExists(item.Player, item.LocationId, item.ItemId, savedItemCounts); 39 | int currentServerCount = EnsureCountExists(item.Player, item.LocationId, item.ItemId, serverSeenItemCounts); 40 | 41 | // if obtaining this item will have sent more items from the server than we have locally, we should receive the item. otherwise we should skip it. 42 | return currentServerCount + 1 > currentSavedCount; 43 | } 44 | 45 | /// 46 | /// Increments the server-side count for the specified item. 47 | /// 48 | /// The object representing the item whose count is to be incremented. This includes details 49 | /// such as the player, location, and item identifier. 50 | public void IncrementServerCountForItem(ItemInfo item) 51 | { 52 | EnsureCountExists(item.Player, item.LocationId, item.ItemId, serverSeenItemCounts); 53 | IncrementCurrentCountForItem(item.Player, item.LocationId, item.ItemId, serverSeenItemCounts); 54 | } 55 | 56 | /// 57 | /// Increments the saved count for a specific item at a given location for a specified player. 58 | /// 59 | /// The identifier of the player for whom the item's saved count is being incremented. 60 | /// The identifier of the location where the item is stored. 61 | /// The identifier of the item whose saved count is being incremented. 62 | public void IncrementSavedCountForItem(int player, long locationId, long itemId) 63 | { 64 | EnsureCountExists(player, locationId, itemId, savedItemCounts); 65 | IncrementCurrentCountForItem(player, locationId, itemId, savedItemCounts); 66 | } 67 | 68 | private static void IncrementCurrentCountForItem(int player, long locationId, long itemId, Dictionary>> itemCounts, int incrementBy = 1) 69 | { 70 | itemCounts[player][locationId][itemId] += incrementBy; 71 | } 72 | 73 | private static int EnsureCountExists(int player, long locationId, long itemId, Dictionary>> itemCounts) 74 | { 75 | if (!itemCounts.TryGetValue(player, out Dictionary> a)) 76 | { 77 | itemCounts[player] = a = new(); 78 | } 79 | 80 | if (!a.TryGetValue(locationId, out Dictionary b)) 81 | { 82 | a[locationId] = b = new(); 83 | } 84 | 85 | if (!b.TryGetValue(itemId, out int count)) 86 | { 87 | b[itemId] = count = 0; 88 | } 89 | return count; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/SlotDataModel/SlotOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Archipelago.HollowKnight.SlotDataModel 2 | { 3 | public class SlotOptions 4 | { 5 | public bool RandomizeDreamers { get; set; } 6 | 7 | public bool RandomizeSkills { get; set; } 8 | 9 | public bool RandomizeFocus { get; set; } 10 | 11 | public bool RandomizeSwim { get; set; } 12 | 13 | public bool RandomizeCharms { get; set; } 14 | 15 | public bool RandomizeKeys { get; set; } 16 | 17 | public bool RandomizeMaskShards { get; set; } 18 | 19 | public bool RandomizeVesselFragments { get; set; } 20 | 21 | public bool RandomizeCharmNotches { get; set; } 22 | 23 | public bool RandomizePaleOre { get; set; } 24 | 25 | public bool RandomizeGeoChests { get; set; } 26 | 27 | public bool RandomizeJunkPitChests { get; set; } 28 | 29 | public bool RandomizeRancidEggs { get; set; } 30 | 31 | public bool RandomizeRelics { get; set; } 32 | 33 | public bool RandomizeWhisperingRoots { get; set; } 34 | 35 | public bool RandomizeBossEssence { get; set; } 36 | 37 | public bool RandomizeGrubs { get; set; } 38 | 39 | public bool RandomizeMimics { get; set; } 40 | 41 | public bool RandomizeMaps { get; set; } 42 | 43 | public bool RandomizeStags { get; set; } 44 | 45 | public bool RandomizeLifebloodCocoons { get; set; } 46 | 47 | public bool RandomizeGrimmkinFlames { get; set; } 48 | 49 | public bool RandomizeJournalEntries { get; set; } 50 | 51 | public bool RandomizeNail { get; set; } 52 | 53 | public bool RandomizeGeoRocks { get; set; } 54 | 55 | public bool RandomizeBossGeo { get; set; } 56 | 57 | public bool RandomizeSoulTotems { get; set; } 58 | 59 | public bool RandomizeLoreTablets { get; set; } 60 | 61 | public bool PreciseMovement { get; set; } 62 | 63 | public bool ProficientCombat { get; set; } 64 | 65 | public bool BackgroundObjectPogos { get; set; } 66 | 67 | public bool EnemyPogos { get; set; } 68 | 69 | public bool ObscureSkips { get; set; } 70 | 71 | public bool ShadeSkips { get; set; } 72 | 73 | public bool InfectionSkips { get; set; } 74 | 75 | public bool FireballSkips { get; set; } 76 | 77 | public bool SpikeTunnels { get; set; } 78 | 79 | public bool AcidSkips { get; set; } 80 | 81 | public bool DamageBoosts { get; set; } 82 | 83 | public bool DangerousSkips { get; set; } 84 | 85 | public bool DarkRooms { get; set; } 86 | 87 | public bool ComplexSkips { get; set; } 88 | 89 | public bool DifficultSkips { get; set; } 90 | 91 | public bool Slopeballs { get; set; } 92 | 93 | public bool ShriekPogos { get; set; } 94 | 95 | public bool RemoveSpellUpgrades { get; set; } 96 | 97 | public bool RandomizeElevatorPass { get; set; } 98 | 99 | public string StartLocationName { get; set; } 100 | 101 | public int MinimumGrubPrice { get; set; } 102 | 103 | public int MaximumGrubPrice { get; set; } 104 | 105 | public int MinimumEssencePrice { get; set; } 106 | 107 | public int MaximumEssencePrice { get; set; } 108 | 109 | public int MinimumEggPrice { get; set; } 110 | 111 | public int MaximumEggPrice { get; set; } 112 | 113 | public int RandomCharmCosts { get; set; } 114 | 115 | public int EggShopSlots { get; set; } 116 | 117 | public GoalsLookup Goal { get; set; } 118 | 119 | public bool DeathLink { get; set; } 120 | 121 | public DeathLinkShadeHandling DeathLinkShade { get; set; } 122 | 123 | public bool DeathLinkBreaksFragileCharms { get; set; } 124 | 125 | public WhitePalaceOption WhitePalace { get; set; } 126 | 127 | public bool ExtraPlatforms { get; set; } 128 | 129 | public int StartingGeo { get; set; } 130 | 131 | public bool SplitMantisClaw { get; set; } 132 | 133 | public bool SplitMothwingCloak { get; set; } 134 | 135 | public bool SplitCrystalHeart { get; set; } 136 | 137 | public bool AddUnshuffledLocations { get; set; } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/CostFactory.cs: -------------------------------------------------------------------------------- 1 | using ItemChanger; 2 | using ItemChanger.Placements; 3 | using ItemChanger.Tags; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace Archipelago.HollowKnight.IC 8 | { 9 | internal class CostFactory 10 | { 11 | private Dictionary> locationCosts; 12 | 13 | /// 14 | /// Initializes a cost factory with costs provided from slot data 15 | /// 16 | /// A lookup from placement name -> cost type -> amount 17 | public CostFactory(Dictionary> locationCosts) 18 | { 19 | this.locationCosts = locationCosts; 20 | } 21 | 22 | public void ApplyCost(AbstractPlacement pmt, AbstractItem item, string serverLocationName) 23 | { 24 | if (locationCosts.TryGetValue(serverLocationName, out Dictionary costs)) 25 | { 26 | List icCosts = new(); 27 | foreach (KeyValuePair entry in costs) 28 | { 29 | Cost proposedCost = null; 30 | switch (entry.Key) 31 | { 32 | case "GEO": 33 | proposedCost = Cost.NewGeoCost(entry.Value); 34 | break; 35 | case "ESSENCE": 36 | proposedCost = Cost.NewEssenceCost(entry.Value); 37 | break; 38 | case "GRUBS": 39 | proposedCost = Cost.NewGrubCost(entry.Value); 40 | break; 41 | case "CHARMS": 42 | proposedCost = new PDIntCost( 43 | entry.Value, nameof(PlayerData.charmsOwned), 44 | $"Acquire {entry.Value} {((entry.Value == 1) ? "charm" : "charms")}" 45 | ); 46 | break; 47 | case "RANCIDEGGS": 48 | proposedCost = new ItemChanger.Modules.CumulativeRancidEggCost(entry.Value); 49 | break; 50 | default: 51 | ArchipelagoMod.Instance.LogWarn( 52 | $"Encountered UNKNOWN currency type {entry.Key} at location {serverLocationName}!"); 53 | break; 54 | } 55 | 56 | if (proposedCost != null) 57 | { 58 | // suppress inherent costs - if the server told us to pay X, but the implementation of 59 | // the location will force us to pay Y >= X, we skip adding the cost to prevent doubling up. 60 | IEnumerable inherentCosts = pmt.GetPlacementAndLocationTags() 61 | .OfType() 62 | .Where(t => t.Inherent) 63 | .Select(t => t.Cost); 64 | if (inherentCosts.Any(c => c.Includes(proposedCost))) 65 | { 66 | ArchipelagoMod.Instance.LogDebug($"Supressing cost {entry.Value} {entry.Key} for location {serverLocationName}"); 67 | continue; 68 | } 69 | else 70 | { 71 | icCosts.Add(proposedCost); 72 | } 73 | } 74 | } 75 | 76 | if (icCosts.Count == 0) 77 | { 78 | ArchipelagoMod.Instance.LogWarn( 79 | $"Found zero cost types when handling placement at location {serverLocationName}!"); 80 | return; 81 | } 82 | 83 | Cost finalCosts; 84 | if (icCosts.Count == 1) 85 | { 86 | finalCosts = icCosts[0]; 87 | } 88 | else 89 | { 90 | finalCosts = new MultiCost(icCosts); 91 | } 92 | 93 | if (pmt is ISingleCostPlacement scp) 94 | { 95 | if (scp.Cost == null) 96 | { 97 | scp.Cost = finalCosts; 98 | } 99 | else 100 | { 101 | scp.Cost = new MultiCost(scp.Cost, finalCosts); 102 | } 103 | } 104 | else 105 | { 106 | CostTag costTag = item.AddTag(); 107 | costTag.Cost = finalCosts; 108 | } 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/Modules/BenchSyncModule.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.MultiClient.Net; 2 | using Archipelago.MultiClient.Net.Enums; 3 | using Archipelago.MultiClient.Net.Models; 4 | using Benchwarp; 5 | using ItemChanger; 6 | using ItemChanger.Modules; 7 | using Newtonsoft.Json.Linq; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | 12 | namespace Archipelago.HollowKnight.IC.Modules 13 | { 14 | public partial class BenchSyncModule : Module 15 | { 16 | private const string DATASTORAGE_KEY_UNLOCKED_BENCHES = "unlocked_benches"; 17 | private const string BENCH_KEY_SEPARATOR = ":::"; 18 | static readonly Dictionary lockedBenches = new() 19 | { 20 | [SceneNames.Hive_01] = "Hive Bench", 21 | [SceneNames.Ruins1_31] = "Toll Machine Bench", 22 | [SceneNames.Abyss_18] = "Toll Machine Bench", 23 | [SceneNames.Fungus3_50] = "Toll Machine Bench" 24 | }; 25 | 26 | private ArchipelagoSession session; 27 | 28 | private Dictionary benchLookup; 29 | 30 | [DataStorageProperty(nameof(session), Scope.Slot, DATASTORAGE_KEY_UNLOCKED_BENCHES)] 31 | private partial DataStorageElement UnlockedBenches { get; set; } 32 | 33 | public override async void Initialize() 34 | { 35 | session = ArchipelagoMod.Instance.session; 36 | benchLookup = Bench.Benches.ToDictionary(x => x.ToBenchKey(), x => x); 37 | 38 | Benchwarp.Events.OnBenchUnlock += OnUnlockLocalBench; 39 | UnlockedBenches.Initialize(JObject.FromObject(new Dictionary())); 40 | 41 | UnlockedBenches.OnValueChanged += OnUnlockRemoteBench; 42 | Dictionary benchData = BuildBenchData(Bench.Benches.Where(x => x.HasVisited()).Select(x => x.ToBenchKey())); 43 | UnlockedBenches += Operation.Update(benchData); 44 | 45 | try 46 | { 47 | Dictionary benches = await UnlockedBenches.GetAsync>(); 48 | UnlockBenches(benches); 49 | } 50 | catch (Exception ex) 51 | { 52 | ArchipelagoMod.Instance.LogError($"Unexpected issue unlocking benches from server data: {ex}"); 53 | } 54 | } 55 | 56 | public override void Unload() 57 | { 58 | Benchwarp.Events.OnBenchUnlock -= OnUnlockLocalBench; 59 | UnlockedBenches.OnValueChanged -= OnUnlockRemoteBench; 60 | } 61 | 62 | private void OnUnlockLocalBench(BenchKey obj) 63 | { 64 | UnlockedBenches += Operation.Update(BuildBenchData([obj])); 65 | } 66 | 67 | private void OnUnlockRemoteBench(JToken oldData, JToken newData, Dictionary args) 68 | { 69 | Dictionary benches = newData.ToObject>(); 70 | UnlockBenches(benches); 71 | } 72 | 73 | private Dictionary BuildBenchData(IEnumerable keys) 74 | { 75 | Dictionary obtainedBenches = new(); 76 | foreach (BenchKey key in keys) 77 | { 78 | obtainedBenches[$"{key.SceneName}{BENCH_KEY_SEPARATOR}{key.RespawnMarkerName}"] = true; 79 | } 80 | return obtainedBenches; 81 | } 82 | 83 | private void UnlockBenches(Dictionary benches) 84 | { 85 | if (benches == null) 86 | { 87 | return; 88 | } 89 | 90 | foreach (KeyValuePair kv in benches) 91 | { 92 | string[] keyParts = kv.Key.Split([BENCH_KEY_SEPARATOR], StringSplitOptions.None); 93 | BenchKey key = new(keyParts[0], keyParts[1]); 94 | if (benchLookup.TryGetValue(key, out Bench bench)) 95 | { 96 | bench.SetVisited(kv.Value); 97 | if (lockedBenches.TryGetValue(bench.sceneName, out string persistentBoolName)) 98 | { 99 | GameManager.instance.sceneData.SaveMyState(new PersistentBoolData() 100 | { 101 | activated = true, 102 | sceneName = bench.sceneName, 103 | semiPersistent = false, 104 | id = persistentBoolName 105 | }); 106 | } 107 | 108 | switch (bench.sceneName) 109 | { 110 | case SceneNames.Room_Tram: 111 | PlayerData.instance.SetBool(nameof(PlayerData.openedTramLower), true); 112 | PlayerData.instance.SetBool(nameof(PlayerData.tramOpenedDeepnest), true); 113 | break; 114 | case SceneNames.Room_Tram_RG: 115 | PlayerData.instance.SetBool(nameof(PlayerData.openedTramRestingGrounds), true); 116 | PlayerData.instance.SetBool(nameof(PlayerData.tramOpenedCrossroads), true); 117 | break; 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/DeathLinkMessages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Archipelago.HollowKnight 5 | { 6 | public static class DeathLinkMessages 7 | { 8 | public static readonly List DefaultMessages = new() 9 | { 10 | "@ died.", 11 | "@ has perished.", 12 | "@ made poor life choices.", 13 | "@ didn't listen to Hornet's advice.", 14 | "@ took damage equal to or more than their current HP.", 15 | "@ made a fatal mistake.", 16 | "@ threw some shade at @.", 17 | "@ decided to set up a Shade Skip.", // A System of Vibrant Colors (edited) 18 | "Hopefully @ didn't have a fragile charm equipped.", // Koatlus 19 | "A true servant gives all for the Kingdom. Let @ relieve you of your life.", // Koatlus 20 | "Through @'s sacrifice, you are now dead.", // Koatlus 21 | "The truce remains. Our vigil holds. @ must respawn.", // Koatlus 22 | "Hopefully @ didn't have a fragile charm equipped.", // Koatlus 23 | }; 24 | 25 | public static readonly List UnknownMessages = new() 26 | { 27 | "@ has died in a manner most unusual.", 28 | "@ found a way to break the game, and the game broke @ back.", 29 | "@ has lost The Game", 30 | }; 31 | 32 | public static readonly Dictionary> MessagesByType = new() 33 | { 34 | { 35 | 1, // Deaths from enemy damage 36 | new List 37 | { 38 | "@ has discovered that there are bugs in Hallownest.", 39 | "@ should have dodged.", 40 | "@ should have jumped.", 41 | "@ significantly mistimed their parry attempt.", 42 | "@ should have considered equipping Dreamshield.", 43 | "@ must have never fought that enemy before.", 44 | "@ did not make it to phase 2.", 45 | "@ dashed in the wrong direction.", // Murphmario 46 | "@ tried to talk it out.", // SnowOfAllTrades 47 | "@ made masterful use of their vulnerability frames.", 48 | } 49 | }, 50 | { 51 | 2, // Deaths from spikes 52 | new List 53 | { 54 | "@ was in the wrong place.", 55 | "@ mistimed their jump.", 56 | "@ didn't see the sharp things.", 57 | "@ didn't see that saw.", 58 | "@ fought the spikes and the spikes won.", 59 | "@ sought roses but found only thorns.", 60 | "@ was pricked to death.", // A System of Vibrant Colors 61 | "@ dashed in the wrong direction.", // Murphmario 62 | "@ found their own Path of Pain.", // Fatman 63 | "@ has strayed from the White King's roads.", // Koatlus 64 | } 65 | }, 66 | { 67 | 3, // Deaths from acid 68 | new List 69 | { 70 | "@ was in the wrong place.", 71 | "@ mistimed their jump.", 72 | "@ forgot their floaties.", 73 | "What @ thought was H2O was H2SO4.", 74 | "@ wishes they could swim.", 75 | "@ used the wrong kind of dive.", 76 | "@ got into a fight with a pool of liquid and lost.", 77 | "@ forgot how to swim", // squidy 78 | } 79 | }, 80 | { 81 | 999, // Deaths in the dream realm 82 | new List 83 | { 84 | "@ dozed off for good.", 85 | "@ was caught sleeping on the job.", 86 | "@ sought dreams but found only nightmares.", 87 | "@ got lost in Limbo.", 88 | "Good night, @.", 89 | "@ is resting in pieces.", 90 | "@ exploded into a thousand pieces of essence.", 91 | "Hey, @, you're finally awake.", 92 | } 93 | }, 94 | }; 95 | 96 | private static readonly Random random = new(); // This is only messaging, so does not need to be seeded. 97 | 98 | public static string GetDeathMessage(int cause, string player) 99 | { 100 | // Build candidate death messages. 101 | List messages; 102 | bool knownCauseOfDeath = MessagesByType.TryGetValue(cause, out messages); 103 | 104 | if (knownCauseOfDeath) 105 | { 106 | messages = new(messages); 107 | messages.AddRange(DefaultMessages); 108 | } 109 | else 110 | { 111 | messages = UnknownMessages; 112 | } 113 | 114 | // Choose one at random 115 | string message = messages[random.Next(0, messages.Count)].Replace("@", player); 116 | 117 | // If it's an unknown death, tag in some debugging info 118 | if (!knownCauseOfDeath) 119 | { 120 | ArchipelagoMod.Instance.LogWarn($"UNKNOWN cause of death {cause}"); 121 | message += $" (Type: {cause})"; 122 | } 123 | 124 | return message; 125 | } 126 | }; 127 | } -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/Modules/GiftingModule.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.Gifting.Net.Service; 2 | using Archipelago.Gifting.Net.Traits; 3 | using Archipelago.Gifting.Net.Versioning.Gifts.Current; 4 | using Archipelago.MultiClient.Net; 5 | using ItemChanger; 6 | using ItemChanger.Modules; 7 | using ItemChanger.Tags; 8 | using MenuChanger; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | 12 | namespace Archipelago.HollowKnight.IC.Modules; 13 | 14 | public class GiftingModule : Module 15 | { 16 | private static readonly string[] AcceptedTraits = [GiftFlag.Mana, GiftFlag.Life, "Artifact"]; 17 | 18 | private ArchipelagoSession session => ArchipelagoMod.Instance.session; 19 | private GiftingService giftingService; 20 | 21 | public override void Initialize() 22 | { 23 | if (ArchipelagoMod.Instance.GS.EnableGifting) 24 | { 25 | giftingService = new GiftingService(session); 26 | On.GameManager.FinishedEnteringScene += BeginGifting; 27 | } 28 | } 29 | 30 | public override void Unload() 31 | { 32 | if (giftingService != null) 33 | { 34 | giftingService.CloseGiftBox(); 35 | giftingService.OnNewGift -= ReceiveOrRefundGift; 36 | giftingService = null; 37 | } 38 | } 39 | 40 | private async void BeginGifting(On.GameManager.orig_FinishedEnteringScene orig, GameManager self) 41 | { 42 | orig(self); 43 | On.GameManager.FinishedEnteringScene -= BeginGifting; 44 | giftingService.OnNewGift += ReceiveOrRefundGift; 45 | Dictionary pendingGifts = await giftingService.CheckGiftBoxAsync(); 46 | foreach (Gift gift in pendingGifts.Values) 47 | { 48 | ReceiveOrRefundGift(gift); 49 | } 50 | giftingService.OpenGiftBox(false, AcceptedTraits); 51 | } 52 | 53 | private void ReceiveOrRefundGift(Gift gift) 54 | { 55 | giftingService.RemoveGiftFromGiftBox(gift.ID); 56 | GiftTrait bestTrait = PickBestMatchingTrait(gift); 57 | if (bestTrait != null) 58 | { 59 | string itemName; 60 | if (bestTrait.Trait == GiftFlag.Mana) 61 | { 62 | // soul refill based on quality. Average is 90 (large totems), below is 54, above is 200, scaling linearly 63 | if (bestTrait.Quality >= 2.2) 64 | { 65 | itemName = ItemNames.Soul_Totem_Path_of_Pain; 66 | } 67 | else if (bestTrait.Quality <= 0.6) 68 | { 69 | itemName = ItemNames.Soul_Totem_B; 70 | } 71 | else 72 | { 73 | itemName = ItemNames.Soul_Totem_A; 74 | } 75 | } 76 | else if (bestTrait.Trait == GiftFlag.Life) 77 | { 78 | // blue hearts based on quality. Average is 2 blue masks, scaling linearly from that 79 | // todo - do we want XL/XS lifeblood for more interesting variance? 80 | if (bestTrait.Quality >= 1.5) 81 | { 82 | itemName = ItemNames.Lifeblood_Cocoon_Large; 83 | } 84 | else 85 | { 86 | itemName = ItemNames.Lifeblood_Cocoon_Small; 87 | } 88 | } 89 | else if (bestTrait.Trait == "Artifact") 90 | { 91 | // relic based on quality (average case is Hallownest seal, scales roughly linearly from that) 92 | if (bestTrait.Quality <= 0.4) 93 | { 94 | itemName = ItemNames.Wanderers_Journal; 95 | } 96 | else if (bestTrait.Quality >= 2.7) 97 | { 98 | itemName = ItemNames.Arcane_Egg; 99 | } 100 | else if (bestTrait.Quality >= 1.8) 101 | { 102 | itemName = ItemNames.Kings_Idol; 103 | } 104 | else 105 | { 106 | itemName = ItemNames.Hallownest_Seal; 107 | } 108 | } 109 | else 110 | { 111 | // safety net in case we update acceptedtraits 112 | ArchipelagoMod.Instance.LogWarn($"Got an unexpected trait {bestTrait} for gift {gift}"); 113 | giftingService.RefundGift(gift); 114 | return; 115 | } 116 | 117 | string sender = session.Players.GetPlayerName(gift.SenderSlot); 118 | DispatchItem(itemName, gift.Amount, sender); 119 | } 120 | else 121 | { 122 | giftingService.RefundGift(gift); 123 | } 124 | } 125 | 126 | private GiftTrait PickBestMatchingTrait(Gift gift) 127 | { 128 | GiftTrait best = null; 129 | foreach (GiftTrait trait in gift.Traits) 130 | { 131 | if (AcceptedTraits.Contains(trait.Trait)) 132 | { 133 | if (best == null || trait.Quality > best.Quality) 134 | { 135 | best = trait; 136 | } 137 | } 138 | } 139 | return best; 140 | } 141 | 142 | private void DispatchItem(string itemName, int amount, string sender) 143 | { 144 | ThreadSupport.BeginInvoke(() => 145 | { 146 | for (int i = 0; i < amount; i++) 147 | { 148 | AbstractItem item = Finder.GetItem(itemName); 149 | InteropTag recentItemsTag = item.AddTag(); 150 | recentItemsTag.Message = "RecentItems"; 151 | recentItemsTag.Properties["DisplaySource"] = sender; 152 | 153 | item.Load(); 154 | item.Give(null, ItemNetworkingModule.RemoteGiveInfo); 155 | item.Unload(); 156 | } 157 | }); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/ArchipelagoTags.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.HollowKnight.IC.Modules; 2 | using Archipelago.MultiClient.Net.Enums; 3 | using Archipelago.MultiClient.Net.Models; 4 | using ItemChanger; 5 | using ItemChanger.Tags; 6 | using System.Collections.Generic; 7 | 8 | namespace Archipelago.HollowKnight.IC 9 | { 10 | /// 11 | /// Tag attached to items that are involved with Archipelago. 12 | /// 13 | /// 14 | /// ArchipelagoItemTags are attached to AP-randomized items to track what their location ID and player ("slot") are. Additionally, they manage events 15 | /// for when items are picked up, ensuring that HK items for other players get replaced out and that all location checks actually get sent. 16 | /// 17 | public class ArchipelagoItemTag : Tag, IInteropTag 18 | { 19 | /// 20 | /// AP location ID for this item. 21 | /// 22 | public long Location { get; set; } 23 | /// 24 | /// AP player ID ("slot") for this item's recipient. 25 | /// 26 | public int Player { get; set; } 27 | /// 28 | /// Network item flags, exposed for benefit of the mapmod 29 | /// 30 | public ItemFlags Flags { get; set; } 31 | 32 | /// 33 | /// Set if this area is hinted. 34 | /// 35 | public bool Hinted { get; set; } = false; 36 | 37 | public bool IsItemForMe { get; set; } 38 | 39 | private ItemNetworkingModule networkModule; 40 | 41 | public void ReadItemInfo(ScoutedItemInfo itemInfo) 42 | { 43 | Location = itemInfo.LocationId; 44 | Player = itemInfo.Player; 45 | Flags = itemInfo.Flags; 46 | 47 | IsItemForMe = itemInfo.IsReceiverRelatedToActivePlayer; 48 | } 49 | 50 | public override async void Load(object parent) 51 | { 52 | base.Load(parent); 53 | networkModule = ItemChangerMod.Modules.Get(); 54 | AbstractItem item = (AbstractItem)parent; 55 | item.AfterGive += AfterGive; 56 | 57 | if (item.WasEverObtained()) 58 | { 59 | await networkModule.SendLocationsAsync(Location); 60 | } 61 | } 62 | 63 | private async void AfterGive(ReadOnlyGiveEventArgs obj) 64 | { 65 | await networkModule.SendLocationsAsync(Location); 66 | } 67 | 68 | public override void Unload(object parent) 69 | { 70 | ((AbstractItem)parent).AfterGive -= AfterGive; 71 | base.Unload(parent); 72 | } 73 | 74 | string IInteropTag.Message => "RandoSupplementalMetadata"; 75 | 76 | bool IInteropTag.TryGetProperty(string propertyName, out T value) where T : default 77 | { 78 | if (propertyName == "ForceEnablePreview" && Hinted is T t) 79 | { 80 | value = t; 81 | return true; 82 | } 83 | 84 | value = default; 85 | return false; 86 | } 87 | } 88 | 89 | /// 90 | /// Tag attached to placements that are involved with Archipelago. 91 | /// 92 | /// 93 | /// ArchipelagoPlacementTags are attached to placements containing AP-randomized. 94 | /// They track whether the placement has been successfully hinted in AP (e.g. when previewed), and what its associated Location ID is. This latter tracking facilitates 95 | /// a dictionary of location IDs to placements so when items are received from our own slot (e.g. same-slot coop or recovering a lost save) we can update the game 96 | /// world accordingly. 97 | /// 98 | public class ArchipelagoPlacementTag : Tag 99 | { 100 | public static Dictionary PlacementsByLocationId = new(); 101 | 102 | /// 103 | /// True if this location has been hinted AP, or is in the process of being hinted. 104 | /// 105 | public bool Hinted { get; set; } 106 | 107 | private HintTracker hintTracker; 108 | 109 | public override void Load(object parent) 110 | { 111 | base.Load(parent); 112 | AbstractPlacement pmt = (AbstractPlacement)parent; 113 | //Archipelago.Instance.LogDebug($"In ArchipelagoPlacementTag:Load for {parent}, locations ({String.Join(", ", PlacementUtils.GetLocationIDs(pmt))})"); 114 | hintTracker = ItemChangerMod.Modules.Get(); 115 | 116 | foreach (long locationId in PlacementUtils.GetLocationIDs(pmt)) 117 | { 118 | PlacementsByLocationId[locationId] = pmt; 119 | } 120 | 121 | pmt.OnVisitStateChanged += OnVisitStateChanged; 122 | // If we've been previewed but never told AP that, tell it now 123 | if (!Hinted && pmt.Visited.HasFlag(VisitState.Previewed)) 124 | { 125 | hintTracker.HintPlacement(pmt); 126 | } 127 | } 128 | 129 | public override void Unload(object parent) 130 | { 131 | //Archipelago.Instance.LogDebug($"In ArchipelagoPlacementTag:UNLOAD for {parent}, locations ({String.Join(", ", PlacementUtils.GetLocationIDs((AbstractPlacement)parent))})"); 132 | ((AbstractPlacement)parent).OnVisitStateChanged -= OnVisitStateChanged; 133 | 134 | foreach (long locationId in PlacementUtils.GetLocationIDs((AbstractPlacement)parent)) 135 | { 136 | PlacementsByLocationId.Remove(locationId); 137 | } 138 | 139 | base.Unload(parent); 140 | } 141 | 142 | private void OnVisitStateChanged(VisitStateChangedEventArgs obj) 143 | { 144 | if (!Hinted && obj.NewFlags.HasFlag(VisitState.Previewed)) 145 | { 146 | // We are now previewed, but we weren't before. 147 | hintTracker.HintPlacement(obj.Placement); 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Resources/Data/starts.json: -------------------------------------------------------------------------------- 1 | { 2 | "King's Pass": { 3 | "name": "King's Pass", 4 | "sceneName": "Tutorial_01", 5 | "x": 35.5, 6 | "y": 11.4, 7 | "zone": "KINGS_PASS", 8 | "transition": "Tutorial_01[right1]", 9 | "logic": "ANY" 10 | }, 11 | "Stag Nest": { 12 | "name": "Stag Nest", 13 | "sceneName": "Cliffs_03", 14 | "x": 85.8, 15 | "y": 46.4, 16 | "zone": "CLIFFS", 17 | "transition": "Cliffs_03[right1]", 18 | "logic": "ANY" 19 | }, 20 | "West Crossroads": { 21 | "name": "West Crossroads", 22 | "sceneName": "Crossroads_36", 23 | "x": 40.2, 24 | "y": 22.0, 25 | "zone": "CROSSROADS", 26 | "transition": "Crossroads_36[right1]", 27 | "logic": "ANY" 28 | }, 29 | "East Crossroads": { 30 | "name": "East Crossroads", 31 | "sceneName": "Crossroads_03", 32 | "x": 14.0, 33 | "y": 68.0, 34 | "zone": "CROSSROADS", 35 | "transition": "Crossroads_03[top1]", 36 | "logic": "ANY" 37 | }, 38 | "Ancestral Mound": { 39 | "name": "Ancestral Mound", 40 | "sceneName": "Crossroads_ShamanTemple", 41 | "x": 37.7, 42 | "y": 46.5, 43 | "zone": "SHAMAN_TEMPLE", 44 | "transition": "Crossroads_ShamanTemple[left1]", 45 | "logic": "ANY" 46 | }, 47 | "West Fog Canyon": { 48 | "name": "West Fog Canyon", 49 | "sceneName": "Fungus3_30", 50 | "x": 35.1, 51 | "y": 16.4, 52 | "zone": "FOG_CANYON", 53 | "transition": "Fungus3_30[bot1]", 54 | "logic": "ANY" 55 | }, 56 | "East Fog Canyon": { 57 | "name": "East Fog Canyon", 58 | "sceneName": "Fungus3_25", 59 | "x": 77.5, 60 | "y": 23.7, 61 | "zone": "FOG_CANYON", 62 | "transition": "Fungus3_25[right1]", 63 | "logic": "ANY" 64 | }, 65 | "Queen's Station": { 66 | "name": "Queen's Station", 67 | "sceneName": "Fungus2_01", 68 | "x": 24.0, 69 | "y": 37.4, 70 | "zone": "QUEENS_STATION", 71 | "transition": "Fungus2_01[left1]", 72 | "logic": "ANY" 73 | }, 74 | "Fungal Wastes": { 75 | "name": "Fungal Wastes", 76 | "sceneName": "Fungus2_28", 77 | "x": 59.6, 78 | "y": 3.4, 79 | "zone": "WASTES", 80 | "transition": "Fungus2_28[left1]", 81 | "logic": "ANY" 82 | }, 83 | "Greenpath": { 84 | "name": "Greenpath", 85 | "sceneName": "Fungus1_32", 86 | "x": 3.8, 87 | "y": 27.4, 88 | "zone": "GREEN_PATH", 89 | "transition": "Fungus1_32[left1]", 90 | "logic": "ANY" 91 | }, 92 | "Lower Greenpath": { 93 | "name": "Lower Greenpath", 94 | "sceneName": "Fungus1_13", 95 | "x": 126.2, 96 | "y": 37.4, 97 | "zone": "GREEN_PATH", 98 | "transition": "Fungus1_13[right1]", 99 | "logic": "ANY" 100 | }, 101 | "West Blue Lake": { 102 | "name": "West Blue Lake", 103 | "sceneName": "Crossroads_50", 104 | "x": 21.2, 105 | "y": 44.4, 106 | "zone": "CROSSROADS", 107 | "transition": "Crossroads_50[left1]", 108 | "logic": "ANY" 109 | }, 110 | "East Blue Lake": { 111 | "name": "East Blue Lake", 112 | "sceneName": "Crossroads_50", 113 | "x": 225.2, 114 | "y": 25.4, 115 | "zone": "CROSSROADS", 116 | "transition": "Crossroads_50[right1]", 117 | "logic": "(ITEMRANDO | MAPAREARANDO) + (ENEMYPOGOS | ELEVATOR) | FULLAREARANDO | ROOMRANDO" 118 | // first branch: ensure upper Resting Grounds or King's Station is accessible 119 | }, 120 | "City Storerooms": { 121 | "name": "City Storerooms", 122 | "sceneName": "Ruins1_17", 123 | "x": 61.6, 124 | "y": 3.4, 125 | "zone": "CITY", 126 | "transition": "Ruins1_17[right1]", 127 | "logic": "ANY" 128 | }, 129 | "King's Station": { 130 | "name": "King's Station", 131 | "sceneName": "Ruins2_10b", 132 | "x": 20.9, 133 | "y": 136.3, 134 | "zone": "CITY", 135 | "transition": "Ruins2_10b[right1]", 136 | "logic": "ANY" 137 | }, 138 | "Outside Colosseum": { 139 | "name": "Outside Colosseum", 140 | "sceneName": "Deepnest_East_09", 141 | "x": 159.9, 142 | "y": 12.4, 143 | "zone": "COLOSSEUM", 144 | "transition": "Deepnest_East_09[right1]", 145 | "logic": "ANY" 146 | }, 147 | "Crystallized Mound": { 148 | "name": "Crystallized Mound", 149 | "sceneName": "Mines_35", 150 | "x": 3.2, 151 | "y": 48.4, 152 | "zone": "MINES", 153 | "transition": "Mines_35[left1]", 154 | "logic": "ANY" 155 | }, 156 | "Mantis Village": { 157 | "name": "Mantis Village", 158 | "sceneName": "Fungus2_14", 159 | "x": 117.8, 160 | "y": 15.4, 161 | "zone": "WASTES", 162 | "transition": "Fungus2_14[right1]", 163 | "logic": "(ITEMRANDO | MAPAREARANDO | FULLAREARANDO) + ENEMYPOGOS | ROOMRANDO | VERTICAL" 164 | // first branch: ensure Queen's Station (or on full area, Fungus2_12[left1]) is reachable 165 | }, 166 | "Kingdom's Edge": { 167 | "name": "Kingdom's Edge", 168 | "sceneName": "Deepnest_East_15", 169 | "x": 26.5, 170 | "y": 4.4, 171 | "zone": "OUTSKIRTS", 172 | "transition": "Deepnest_East_15[left1]", 173 | "logic": "(ITEMRANDO + ENEMYPOGOS + SWIM) | MAPAREARANDO | FULLAREARANDO | ROOMRANDO" 174 | // first branch: ensure King's Station Stag is reachable 175 | }, 176 | "Hallownest's Crown": { 177 | "name": "Hallownest's Crown", 178 | "sceneName": "Mines_34", 179 | "x": 128.3, 180 | "y": 46.4, 181 | "zone": "MINES", 182 | "transition": "Mines_34[bot1]", 183 | "logic": "(ITEMRANDO | MAPAREARANDO) + DARKROOMS | FULLAREARANDO | ROOMRANDO" 184 | }, 185 | "West Waterways": { 186 | "name": "West Waterways", 187 | "sceneName": "Waterways_09", 188 | "x": 34.7, 189 | "y": 30.4, 190 | "zone": "WATERWAYS", 191 | "transition": "Waterways_09[left1]", 192 | "logic": "(ITEMRANDO + ENEMYPOGOS + SHADESKIPS + 2MASKS) | MAPAREARANDO | FULLAREARANDO | ROOMRANDO" 193 | }, 194 | "Queen's Gardens": { 195 | "name": "Queen's Gardens", 196 | "sceneName": "Fungus3_13", 197 | "x": 25.3, 198 | "y": 63.4, 199 | "zone": "ROYAL_GARDENS", 200 | "transition": "Fungus3_13[left1]", 201 | "logic": "(ITEMRANDO | MAPAREARANDO | FULLAREARANDO) + ENEMYPOGOS + DANGEROUSSKIPS | ROOMRANDO" 202 | // skip logic is minimum to ensure Hallownest_Seal-Queen's_Gardens is reachable 203 | }, 204 | "Distant Village": { 205 | "name": "Distant Village", 206 | "sceneName": "Room_spider_small", 207 | "x": 23.1, 208 | "y": 13.4, 209 | "zone": "DEEPNEST", 210 | "transition": "Room_spider_small[left1]", 211 | "logic": "FULLAREARANDO | ROOMRANDO" 212 | }, 213 | "Far Greenpath": { 214 | "name": "Far Greenpath", 215 | "sceneName": "Fungus1_13", 216 | "x": 34.9, 217 | "y": 23.4, 218 | "zone": "GREEN_PATH", 219 | "transition": "Fungus1_13[left1]", 220 | "logic": "MAPAREARANDO | FULLAREARANDO | ROOMRANDO" 221 | }, 222 | "Hive": { 223 | "name": "Hive", 224 | "sceneName": "Hive_03", 225 | "x": 47.2, 226 | "y": 142.7, 227 | "zone": "HIVE", 228 | "transition": "Hive_03[right1]", 229 | "logic": "ROOMRANDO" 230 | }, 231 | "Royal Waterways": { 232 | "name": "Royal Waterways", 233 | "sceneName": "Waterways_03", 234 | "x": 93.6, 235 | "y": 4.4, 236 | "zone": "WATERWAYS", 237 | "transition": "Waterways_03[left1]", 238 | "logic": "ROOMRANDO" 239 | }, 240 | "City of Tears": { 241 | "name": "City of Tears", 242 | "sceneName": "Ruins1_27", 243 | "x": 29.6, 244 | "y": 6.4, 245 | "zone": "CITY", 246 | "transition": "Ruins1_27[left1]", 247 | "logic": "ROOMRANDO" 248 | }, 249 | "Abyss": { 250 | "name": "Abyss", 251 | "sceneName": "Abyss_06_Core", 252 | "x": 42.0, 253 | "y": 5.4, 254 | "zone": "ABYSS", 255 | "transition": "Abyss_06_Core[right2]", 256 | "logic": "ROOMRANDO" 257 | }, 258 | "Fungal Core": { 259 | "name": "Fungal Core", 260 | "sceneName": "Fungus2_30", 261 | "x": 64.8, 262 | "y": 21.4, 263 | "zone": "WASTES", 264 | "transition": "Fungus2_30[top1]", 265 | "logic": "ROOMRANDO" 266 | } 267 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | DLLs/* 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | ## 6 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 7 | 8 | # User-specific files 9 | *.rsuser 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Mono auto generated files 19 | mono_crash.* 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | [Ww][Ii][Nn]32/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | [Ll]ogs/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # ASP.NET Scaffolding 68 | ScaffoldingReadMe.txt 69 | 70 | # StyleCop 71 | StyleCopReport.xml 72 | 73 | # Files built by Visual Studio 74 | *_i.c 75 | *_p.c 76 | *_h.h 77 | *.ilk 78 | *.meta 79 | *.obj 80 | *.iobj 81 | *.pch 82 | *.pdb 83 | *.ipdb 84 | *.pgc 85 | *.pgd 86 | *.rsp 87 | *.sbr 88 | *.tlb 89 | *.tli 90 | *.tlh 91 | *.tmp 92 | *.tmp_proj 93 | *_wpftmp.csproj 94 | *.log 95 | *.tlog 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 300 | *.vbp 301 | 302 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 303 | *.dsw 304 | *.dsp 305 | 306 | # Visual Studio 6 technical files 307 | *.ncb 308 | *.aps 309 | 310 | # Visual Studio LightSwitch build output 311 | **/*.HTMLClient/GeneratedArtifacts 312 | **/*.DesktopClient/GeneratedArtifacts 313 | **/*.DesktopClient/ModelManifest.xml 314 | **/*.Server/GeneratedArtifacts 315 | **/*.Server/ModelManifest.xml 316 | _Pvt_Extensions 317 | 318 | # Paket dependency manager 319 | .paket/paket.exe 320 | paket-files/ 321 | 322 | # FAKE - F# Make 323 | .fake/ 324 | 325 | # CodeRush personal settings 326 | .cr/personal 327 | 328 | # Python Tools for Visual Studio (PTVS) 329 | __pycache__/ 330 | *.pyc 331 | 332 | # Cake - Uncomment if you are using it 333 | # tools/** 334 | # !tools/packages.config 335 | 336 | # Tabs Studio 337 | *.tss 338 | 339 | # Telerik's JustMock configuration file 340 | *.jmconfig 341 | 342 | # BizTalk build output 343 | *.btp.cs 344 | *.btm.cs 345 | *.odx.cs 346 | *.xsd.cs 347 | 348 | # OpenCover UI analysis results 349 | OpenCover/ 350 | 351 | # Azure Stream Analytics local run output 352 | ASALocalRun/ 353 | 354 | # MSBuild Binary and Structured Log 355 | *.binlog 356 | 357 | # NVidia Nsight GPU debugger configuration file 358 | *.nvuser 359 | 360 | # MFractors (Xamarin productivity tool) working folder 361 | .mfractor/ 362 | 363 | # Local History for Visual Studio 364 | .localhistory/ 365 | 366 | # Visual Studio History (VSHistory) files 367 | .vshistory/ 368 | 369 | # BeatPulse healthcheck temp database 370 | healthchecksdb 371 | 372 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 373 | MigrationBackup/ 374 | 375 | # Ionide (cross platform F# VS Code tools) working folder 376 | .ionide/ 377 | 378 | # Fody - auto-generated XML schema 379 | FodyWeavers.xsd 380 | 381 | # VS Code files for those working on multiple tools 382 | .vscode/* 383 | !.vscode/settings.json 384 | !.vscode/tasks.json 385 | !.vscode/launch.json 386 | !.vscode/extensions.json 387 | *.code-workspace 388 | 389 | # Local History for Visual Studio Code 390 | .history/ 391 | 392 | # Windows Installer files from build outputs 393 | *.cab 394 | *.msi 395 | *.msix 396 | *.msm 397 | *.msp 398 | 399 | # JetBrains Rider 400 | *.sln.iml 401 | 402 | LocalOverrides.targets -------------------------------------------------------------------------------- /Archipelago.HollowKnight/HintTracker.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.HollowKnight.IC; 2 | using Archipelago.HollowKnight.IC.Modules; 3 | using Archipelago.MultiClient.Net; 4 | using Archipelago.MultiClient.Net.Enums; 5 | using Archipelago.MultiClient.Net.Exceptions; 6 | using Archipelago.MultiClient.Net.Models; 7 | using ItemChanger; 8 | using ItemChanger.Modules; 9 | using ItemChanger.Placements; 10 | using ItemChanger.Tags; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | using System.Threading.Tasks; 15 | using UnityEngine.SceneManagement; 16 | 17 | namespace Archipelago.HollowKnight; 18 | 19 | public class HintTracker : Module 20 | { 21 | 22 | public static event Action OnArchipelagoHintUpdate; 23 | 24 | /// 25 | /// List of MultiClient.Net Hint's 26 | /// 27 | public static List Hints; 28 | /// 29 | /// List of placement hints to send on scene change or when closing out the session 30 | /// 31 | private List PendingPlacementHints; 32 | 33 | private ArchipelagoSession session; 34 | 35 | private void UpdateHints(Hint[] arrayHints) 36 | { 37 | 38 | Hints = arrayHints.ToList(); 39 | foreach (Hint hint in Hints) 40 | { 41 | if (hint.FindingPlayer != ArchipelagoMod.Instance.session.ConnectionInfo.Slot) 42 | { 43 | continue; 44 | } 45 | 46 | if (!ArchipelagoPlacementTag.PlacementsByLocationId.ContainsKey(hint.LocationId)) 47 | { 48 | continue; 49 | } 50 | 51 | ArchipelagoMod.Instance.LogDebug($"Hint data received for item {hint.ItemId} at location {hint.LocationId}"); 52 | 53 | AbstractPlacement placement = ArchipelagoPlacementTag.PlacementsByLocationId[hint.LocationId]; 54 | 55 | if (placement == null) 56 | { 57 | continue; 58 | } 59 | 60 | // set the hinted tag for the single item in the placement that was hinted for. 61 | foreach (ArchipelagoItemTag tag in placement.Items.Select(item => item.GetTag()) 62 | .Where(tag => tag.Location == hint.LocationId)) 63 | { 64 | ArchipelagoMod.Instance.LogDebug("Setting hinted true for item"); 65 | tag.Hinted = true; 66 | } 67 | 68 | // if all items inside a placement have been hinted for then mark the entire placement as hinted. 69 | if (placement.Items.TrueForAll(item => item.GetTag().Hinted)) 70 | { 71 | ArchipelagoMod.Instance.LogDebug("Setting hinted true for placement"); 72 | placement.GetTag().Hinted = true; 73 | } 74 | 75 | if (placement is ShopPlacement shop) 76 | { 77 | List<(string, AbstractItem)> previewText = new(); 78 | foreach (AbstractItem item in shop.Items) 79 | { 80 | if (item.GetTag().Hinted) 81 | { 82 | previewText.Add((item.GetPreviewWithCost(), item)); 83 | } 84 | else 85 | { 86 | previewText.Add((Language.Language.Get("???", "IC"), item)); 87 | } 88 | 89 | } 90 | MultiPreviewRecordTag previewRecordTag = shop.GetOrAddTag(); 91 | previewRecordTag.previewTexts ??= new string[shop.Items.Count]; 92 | 93 | foreach ((string, AbstractItem item) p in previewText) 94 | { 95 | string str = p.Item1; 96 | int index = shop.Items.IndexOf(p.item); 97 | if (index >= 0) 98 | { 99 | previewRecordTag.previewTexts[index] = str; 100 | } 101 | } 102 | } 103 | else 104 | { 105 | List previewText = new(); 106 | foreach (AbstractItem item in placement.Items) 107 | { 108 | if (item.WasEverObtained()) 109 | { 110 | continue; 111 | } 112 | 113 | previewText.Add(item.GetTag().Hinted 114 | ? item.GetPreviewWithCost() 115 | : Language.Language.Get("???", "IC")); 116 | } 117 | 118 | placement.GetOrAddTag().previewText = string.Join(Language.Language.Get("COMMA_SPACE", "IC"), previewText); 119 | } 120 | 121 | } 122 | 123 | try 124 | { 125 | OnArchipelagoHintUpdate?.Invoke(); 126 | } 127 | catch (Exception ex) 128 | { 129 | ArchipelagoMod.Instance.LogError($"Error invoking OnArchipelagoHintUpdate:\n {ex}"); 130 | } 131 | } 132 | 133 | public override void Initialize() 134 | { 135 | PendingPlacementHints = []; 136 | 137 | session = ArchipelagoMod.Instance.session; 138 | 139 | // do most setup in OnEnterGame so save data can completely load, we need to 140 | // populate all the AP placements to sync with server 141 | Events.OnEnterGame += OnEnterGame; 142 | } 143 | 144 | private void OnEnterGame() 145 | { 146 | session.DataStorage.TrackHints(UpdateHints); 147 | 148 | AbstractItem.AfterGiveGlobal += UpdateHintFoundStatus; 149 | Events.OnSceneChange += SendHintsOnSceneChange; 150 | } 151 | 152 | public override async void Unload() 153 | { 154 | Events.OnEnterGame -= OnEnterGame; 155 | AbstractItem.AfterGiveGlobal -= UpdateHintFoundStatus; 156 | Events.OnSceneChange -= SendHintsOnSceneChange; 157 | await SendPlacementHintsAsync(); 158 | } 159 | 160 | public void HintPlacement(AbstractPlacement pmt) 161 | { 162 | // todo - accommodate different hinting times (immediate/never) 163 | PendingPlacementHints.Add(pmt); 164 | } 165 | 166 | private void UpdateHintFoundStatus(ReadOnlyGiveEventArgs args) 167 | { 168 | if (Hints != null && args.Orig.GetTag(out ArchipelagoItemTag tag)) 169 | { 170 | long location = tag.Location; 171 | foreach (Hint hint in Hints) 172 | { 173 | if (hint.LocationId == location) 174 | { 175 | hint.Found = true; 176 | try 177 | { 178 | OnArchipelagoHintUpdate?.Invoke(); 179 | } 180 | catch (Exception ex) 181 | { 182 | ArchipelagoMod.Instance.LogError($"Error invoking OnArchipelagoHintUpdate:\n {ex}"); 183 | } 184 | break; 185 | } 186 | } 187 | } 188 | } 189 | 190 | private async void SendHintsOnSceneChange(Scene scene) 191 | { 192 | await SendPlacementHintsAsync(); 193 | } 194 | 195 | private async Task SendPlacementHintsAsync() 196 | { 197 | if (!PendingPlacementHints.Any()) 198 | { 199 | return; 200 | } 201 | 202 | HashSet hintedTags = new(); 203 | HashSet hintedLocationIDs = new(); 204 | ArchipelagoItemTag tag; 205 | 206 | foreach (AbstractPlacement pmt in PendingPlacementHints) 207 | { 208 | foreach (AbstractItem item in pmt.Items) 209 | { 210 | if (item.GetTag(out tag) && !tag.Hinted) 211 | { 212 | if ((tag.Flags.HasFlag(ItemFlags.Advancement) || tag.Flags.HasFlag(ItemFlags.NeverExclude)) 213 | && !item.WasEverObtained() 214 | && !item.HasTag()) 215 | { 216 | hintedTags.Add(tag); 217 | hintedLocationIDs.Add(tag.Location); 218 | } 219 | else 220 | { 221 | tag.Hinted = true; 222 | } 223 | } 224 | } 225 | } 226 | 227 | PendingPlacementHints.Clear(); 228 | if (!hintedLocationIDs.Any()) 229 | { 230 | return; 231 | } 232 | 233 | ArchipelagoMod.Instance.LogDebug($"Hinting {hintedLocationIDs.Count()} locations."); 234 | try 235 | { 236 | await session.Locations.ScoutLocationsAsync(true, hintedLocationIDs.ToArray()) 237 | .ContinueWith(x => 238 | { 239 | bool result = !x.IsFaulted; 240 | foreach (ArchipelagoItemTag tag in hintedTags) 241 | { 242 | tag.Hinted = result; 243 | } 244 | }).TimeoutAfter(1000); 245 | } 246 | catch (Exception ex) when (ex is ArchipelagoSocketClosedException or TimeoutException) 247 | { 248 | ItemChangerMod.Modules.Get().ReportDisconnect(); 249 | } 250 | } 251 | } -------------------------------------------------------------------------------- /Archipelago.HollowKnight/MC/ArchipelagoModeMenuConstructor.cs: -------------------------------------------------------------------------------- 1 | using MenuChanger; 2 | using MenuChanger.Extensions; 3 | using MenuChanger.MenuElements; 4 | using MenuChanger.MenuPanels; 5 | using Modding; 6 | using System; 7 | using UnityEngine; 8 | 9 | namespace Archipelago.HollowKnight.MC 10 | { 11 | internal class ArchipelagoModeMenuConstructor : ModeMenuConstructor 12 | { 13 | private MenuPage modeConfigPage; 14 | 15 | private readonly static Type _settingsType = typeof(ConnectionDetails); 16 | private readonly static Font _perpetua = CanvasUtil.GetFont("Perpetua"); 17 | 18 | public override void OnEnterMainMenu(MenuPage modeMenu) 19 | { 20 | modeConfigPage = new MenuPage("Archipelago Settings", modeMenu); 21 | ConnectionDetails settings = ArchipelagoMod.Instance.GS.MenuConnectionDetails; 22 | 23 | EntryField urlField = CreateUrlField(modeConfigPage, settings); 24 | NumericEntryField portField = CreatePortField(modeConfigPage, settings); 25 | EntryField nameField = CreateSlotNameField(modeConfigPage, settings); 26 | EntryField passwordField = CreatePasswordField(modeConfigPage, settings); 27 | 28 | MenuLabel errorLabel = new(modeConfigPage, ""); 29 | BigButton startButton = new(modeConfigPage, "Start", "May stall after clicking"); 30 | 31 | startButton.AddSetResumeKeyEvent("Archipelago"); 32 | startButton.OnClick += () => StartOrResumeGame(true, errorLabel); 33 | 34 | modeConfigPage.AfterHide += () => errorLabel.Text.text = ""; 35 | 36 | IMenuElement[] elements = 37 | [ 38 | urlField, 39 | portField, 40 | nameField, 41 | passwordField, 42 | startButton, 43 | errorLabel 44 | ]; 45 | VerticalItemPanel vip = new(modeConfigPage, SpaceParameters.TOP_CENTER_UNDER_TITLE, 100, false, elements); 46 | modeConfigPage.AddToNavigationControl(vip); 47 | 48 | AttachResumePage(); 49 | } 50 | 51 | private void AttachResumePage() 52 | { 53 | MenuPage resumePage = new("Archipelago Resume"); 54 | 55 | MenuLabel slotName = new(resumePage, ""); 56 | 57 | EntryField urlField = CreateUrlField(resumePage, null); 58 | NumericEntryField portField = CreatePortField(resumePage, null); 59 | EntryField passwordField = CreatePasswordField(resumePage, null); 60 | 61 | SmallButton resumeButton = new(resumePage, "Resume"); 62 | MenuLabel errorLabel = new(resumePage, ""); 63 | 64 | void RebindSettings() 65 | { 66 | ConnectionDetails settings = ArchipelagoMod.Instance.LS.ConnectionDetails; 67 | if (settings != null) 68 | { 69 | slotName.Text.text = $"Slot Name: {settings.SlotName}"; 70 | urlField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerUrl))); 71 | portField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerPort))); 72 | passwordField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerPassword))); 73 | } 74 | else 75 | { 76 | slotName.Text.text = "Incompatible save file"; 77 | errorLabel.Text.text = "To resume, recreate your save or downgrade to an older client version."; 78 | } 79 | } 80 | 81 | resumeButton.OnClick += () => StartOrResumeGame(false, errorLabel); 82 | 83 | resumePage.BeforeShow += RebindSettings; 84 | resumePage.AfterHide += () => errorLabel.Text.text = ""; 85 | 86 | IMenuElement[] elements = 87 | [ 88 | slotName, 89 | urlField, 90 | portField, 91 | passwordField, 92 | resumeButton, 93 | errorLabel 94 | ]; 95 | 96 | VerticalItemPanel vip = new(resumePage, SpaceParameters.TOP_CENTER_UNDER_TITLE, 100, true, elements); 97 | resumePage.AddToNavigationControl(vip); 98 | 99 | ResumeMenu.AddResumePage("Archipelago", resumePage); 100 | } 101 | 102 | private static EntryField CreateUrlField(MenuPage apPage, ConnectionDetails settings) 103 | { 104 | EntryField urlField = new(apPage, "Server URL: "); 105 | urlField.InputField.characterLimit = 500; 106 | RectTransform urlRect = urlField.InputField.gameObject.transform.Find("Text").GetComponent(); 107 | urlRect.sizeDelta = new Vector2(1500f, 63.2f); 108 | urlField.InputField.textComponent.font = _perpetua; 109 | if (settings != null) 110 | { 111 | urlField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerUrl))); 112 | } 113 | return urlField; 114 | } 115 | 116 | private static NumericEntryField CreatePortField(MenuPage apPage, ConnectionDetails settings) 117 | { 118 | NumericEntryField portField = new(apPage, "Server Port: "); 119 | portField.SetClamp(0, 65535); 120 | portField.InputField.textComponent.font = _perpetua; 121 | if (settings != null) 122 | { 123 | portField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerPort))); 124 | } 125 | return portField; 126 | } 127 | 128 | private static EntryField CreateSlotNameField(MenuPage apPage, ConnectionDetails settings) 129 | { 130 | EntryField nameField = new(apPage, "Slot Name: "); 131 | nameField.InputField.characterLimit = 500; 132 | nameField.InputField.textComponent.font = _perpetua; 133 | RectTransform nameRect = nameField.InputField.gameObject.transform.Find("Text").GetComponent(); 134 | nameRect.sizeDelta = new Vector2(1500f, 63.2f); 135 | if (settings != null) 136 | { 137 | nameField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.SlotName))); 138 | } 139 | return nameField; 140 | } 141 | 142 | private static EntryField CreatePasswordField(MenuPage apPage, ConnectionDetails settings) 143 | { 144 | EntryField passwordField = new(apPage, "Password: "); 145 | passwordField.InputField.characterLimit = 500; 146 | passwordField.InputField.textComponent.font = _perpetua; 147 | RectTransform passwordRect = passwordField.InputField.gameObject.transform.Find("Text").GetComponent(); 148 | passwordRect.sizeDelta = new Vector2(1500f, 63.2f); 149 | if (settings != null) 150 | { 151 | passwordField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerPassword))); 152 | } 153 | return passwordField; 154 | } 155 | 156 | private static void StartOrResumeGame(bool newGame, MenuLabel errorLabel) 157 | { 158 | ArchipelagoMod.Instance.ArchipelagoEnabled = true; 159 | 160 | // Cloning some settings onto others depending on what is taking precedence. 161 | // If it's a save slot we're resuming (newGame == false) then we want the slot settings to overwrite the global ones. 162 | if (newGame) 163 | { 164 | ArchipelagoMod.Instance.LS = new APLocalSettings() 165 | { 166 | ConnectionDetails = ArchipelagoMod.Instance.GS.MenuConnectionDetails with { }, 167 | }; 168 | } 169 | else if (ArchipelagoMod.Instance.LS.ConnectionDetails != null) 170 | { 171 | ArchipelagoMod.Instance.GS.MenuConnectionDetails = ArchipelagoMod.Instance.LS.ConnectionDetails with { }; 172 | } 173 | 174 | try 175 | { 176 | ArchipelagoMod.Instance.StartOrResumeGame(newGame); 177 | MenuChangerMod.HideAllMenuPages(); 178 | if (newGame) 179 | { 180 | UIManager.instance.StartNewGame(); 181 | } 182 | else 183 | { 184 | UIManager.instance.ContinueGame(); 185 | GameManager.instance.ContinueGame(); 186 | } 187 | } 188 | catch (LoginValidationException ex) 189 | { 190 | ArchipelagoMod.Instance.DisconnectArchipelago(); 191 | errorLabel.Text.text = ex.Message; 192 | } 193 | catch (Exception ex) 194 | { 195 | errorLabel.Text.text = "An unknown error occurred when attempting to connect."; 196 | ArchipelagoMod.Instance.LogError(ex); 197 | ArchipelagoMod.Instance.DisconnectArchipelago(); 198 | } 199 | } 200 | 201 | public override void OnExitMainMenu() 202 | { 203 | modeConfigPage = null; 204 | } 205 | 206 | public override bool TryGetModeButton(MenuPage modeMenu, out BigButton button) 207 | { 208 | button = new BigButton(modeMenu, ArchipelagoMod.Instance.spriteManager.GetSprite("IconColorBig"), "Archipelago"); 209 | button.AddHideAndShowEvent(modeMenu, modeConfigPage); 210 | return true; 211 | } 212 | } 213 | } -------------------------------------------------------------------------------- /Archipelago.HollowKnight/ArchipelagoMod.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.HollowKnight.IC.Modules; 2 | using Archipelago.HollowKnight.MC; 3 | using Archipelago.HollowKnight.SlotDataModel; 4 | using Archipelago.MultiClient.Net; 5 | using Archipelago.MultiClient.Net.Enums; 6 | using ItemChanger; 7 | using ItemChanger.Internal; 8 | using Modding; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.IO; 12 | using System.Linq; 13 | using System.Security.Cryptography; 14 | using System.Text; 15 | using UnityEngine; 16 | 17 | namespace Archipelago.HollowKnight 18 | { 19 | public class ArchipelagoMod : Mod, IGlobalSettings, ILocalSettings, IMenuMod 20 | { 21 | // Events support 22 | public static event Action OnArchipelagoGameStarted; 23 | public static event Action OnArchipelagoGameEnded; 24 | 25 | /// 26 | /// Minimum Archipelago Protocol Version 27 | /// 28 | private readonly Version ArchipelagoProtocolVersion = new(0, 5, 0); 29 | 30 | /// 31 | /// Mod version as reported to the modding API 32 | /// 33 | public override string GetVersion() 34 | { 35 | Version assemblyVersion = GetType().Assembly.GetName().Version; 36 | string version = $"{assemblyVersion.Major}.{assemblyVersion.Minor}.{assemblyVersion.Build}"; 37 | #if DEBUG 38 | using SHA1 sha = SHA1.Create(); 39 | using FileStream str = File.OpenRead(GetType().Assembly.Location); 40 | StringBuilder sb = new(); 41 | foreach (byte b in sha.ComputeHash(str).Take(4)) 42 | { 43 | sb.AppendFormat("{0:x2}", b); 44 | } 45 | version += "-prerelease+" + sb.ToString(); 46 | #endif 47 | return version; 48 | } 49 | public static ArchipelagoMod Instance; 50 | public ArchipelagoSession session { get; private set; } 51 | public SlotData SlotData { get; private set; } 52 | public bool ArchipelagoEnabled { get; set; } 53 | 54 | public bool ToggleButtonInsideMenu => false; 55 | 56 | internal SpriteManager spriteManager; 57 | 58 | internal APGlobalSettings GS = new(); 59 | internal APLocalSettings LS = new(); 60 | 61 | public ArchipelagoMod() : base("Archipelago") { } 62 | 63 | public override void Initialize(Dictionary> preloadedObjects) 64 | { 65 | base.Initialize(); 66 | Log("Initializing"); 67 | Instance = this; 68 | spriteManager = new SpriteManager(typeof(ArchipelagoMod).Assembly, "Archipelago.HollowKnight.Resources."); 69 | 70 | MenuChanger.ModeMenu.AddMode(new ArchipelagoModeMenuConstructor()); 71 | Log("Initialized"); 72 | } 73 | 74 | public void EndGame() 75 | { 76 | LogDebug("Ending Archipelago game"); 77 | try 78 | { 79 | OnArchipelagoGameEnded?.Invoke(); 80 | } 81 | catch (Exception ex) 82 | { 83 | LogError($"Error invoking OnArchipelagoGameEnded:\n {ex}"); 84 | } 85 | 86 | DisconnectArchipelago(); 87 | ArchipelagoEnabled = false; 88 | LS = new(); 89 | 90 | Events.OnItemChangerUnhook -= EndGame; 91 | } 92 | 93 | /// 94 | /// Call when starting or resuming a game to randomize and restore state. 95 | /// 96 | public void StartOrResumeGame(bool randomize) 97 | { 98 | if (!ArchipelagoEnabled) 99 | { 100 | LogDebug("StartOrResumeGame: This is not an Archipelago Game, so not doing anything."); 101 | return; 102 | } 103 | 104 | LogDebug("StartOrResumeGame: This is an Archipelago Game."); 105 | 106 | LoginSuccessful loginResult = ConnectToArchipelago(); 107 | 108 | if (randomize) 109 | { 110 | LogDebug("StartOrResumeGame: Beginning first time randomization."); 111 | LS.Seed = SlotData.Seed; 112 | LS.RoomSeed = session.RoomState.Seed; 113 | 114 | LogDebug($"StartOrResumeGame: Room: {LS.RoomSeed}; Seed = {LS.RoomSeed}"); 115 | 116 | ArchipelagoRandomizer randomizer = new(SlotData); 117 | randomizer.Randomize(); 118 | } 119 | else 120 | { 121 | LogDebug($"StartOrResumeGame: Local : Room: {LS.RoomSeed}; Seed = {LS.Seed}"); 122 | int seed = SlotData.Seed; 123 | LogDebug($"StartOrResumeGame: AP : Room: {session.RoomState.Seed}; Seed = {seed}"); 124 | if (seed != LS.Seed || session.RoomState.Seed != LS.RoomSeed) 125 | { 126 | throw new LoginValidationException("Slot mismatch. Saved seed does not match the server value. Is this the correct save?"); 127 | } 128 | } 129 | 130 | // check the goal is one we know how to cope with 131 | if (SlotData.Options.Goal > GoalsLookup.MAX) 132 | { 133 | throw new LoginValidationException($"Unrecognized goal condition {SlotData.Options.Goal} (are you running an outdated client?)"); 134 | } 135 | 136 | // Hooks happen after we've definitively connected to an Archipelago slot correctly. 137 | // Doing this before checking for the correct slot/seed/room will cause problems if 138 | // the client connects to the wrong session with a matching slot. 139 | Events.OnItemChangerUnhook += EndGame; 140 | 141 | try 142 | { 143 | OnArchipelagoGameStarted?.Invoke(); 144 | } 145 | catch (Exception ex) 146 | { 147 | LogError($"Error invoking OnArchipelagoGameStarted:\n {ex}"); 148 | } 149 | } 150 | 151 | private void OnSocketClosed(string reason) 152 | { 153 | ItemChangerMod.Modules.Get().ReportDisconnect(); 154 | } 155 | 156 | private LoginSuccessful ConnectToArchipelago() 157 | { 158 | session = ArchipelagoSessionFactory.CreateSession(LS.ConnectionDetails.ServerUrl, LS.ConnectionDetails.ServerPort); 159 | 160 | LoginResult loginResult = session.TryConnectAndLogin("Hollow Knight", 161 | LS.ConnectionDetails.SlotName, 162 | ItemsHandlingFlags.AllItems, 163 | ArchipelagoProtocolVersion, 164 | password: LS.ConnectionDetails.ServerPassword, 165 | requestSlotData: false); 166 | 167 | if (loginResult is LoginFailure failure) 168 | { 169 | string errors = string.Join(", ", failure.Errors); 170 | LogError($"Unable to connect to Archipelago because: {string.Join(", ", failure.Errors)}"); 171 | throw new LoginValidationException(errors); 172 | } 173 | else if (loginResult is LoginSuccessful success) 174 | { 175 | SlotData = session.DataStorage.GetSlotData(); 176 | session.Socket.SocketClosed += OnSocketClosed; 177 | 178 | return success; 179 | } 180 | else 181 | { 182 | LogError($"Unexpected LoginResult type when connecting to Archipelago: {loginResult}"); 183 | throw new LoginValidationException("Unexpected login result."); 184 | } 185 | } 186 | 187 | public void DisconnectArchipelago() 188 | { 189 | if (session?.Socket != null) 190 | { 191 | session.Socket.SocketClosed -= OnSocketClosed; 192 | } 193 | 194 | if (session?.Socket != null && session.Socket.Connected) 195 | { 196 | session.Socket.DisconnectAsync(); 197 | } 198 | 199 | session = null; 200 | } 201 | 202 | /// 203 | /// Called when loading local (game-specific save data) 204 | /// 205 | /// 206 | /// This is also called on the main menu screen with empty (defaulted) ConnectionDetails. This will have an empty SlotName, so we treat this as a noop. 207 | /// 208 | /// 209 | public void OnLoadLocal(APLocalSettings ls) 210 | { 211 | if (ls.ConnectionDetails == null 212 | || ls.ConnectionDetails.SlotName == null 213 | || ls.ConnectionDetails.SlotName == "") // Apparently, this is called even before a save is loaded. Catch this. 214 | { 215 | return; 216 | } 217 | 218 | LS = ls; 219 | } 220 | 221 | /// 222 | /// Called when saving local (game-specific) save data. 223 | /// 224 | /// 225 | public APLocalSettings OnSaveLocal() 226 | { 227 | if (!ArchipelagoEnabled) 228 | { 229 | return default; 230 | } 231 | 232 | return LS; 233 | } 234 | 235 | /// 236 | /// Called when loading global save data. 237 | /// 238 | /// 239 | /// For simplicity's sake, we use the same data structure for both global and local save data, though not all fields are relevant in the global context. 240 | /// 241 | /// 242 | public void OnLoadGlobal(APGlobalSettings gs) 243 | { 244 | GS = gs; 245 | } 246 | 247 | /// 248 | /// Called when saving global save data. 249 | /// 250 | /// 251 | public APGlobalSettings OnSaveGlobal() 252 | { 253 | APGlobalSettings r = GS with 254 | { 255 | MenuConnectionDetails = GS.MenuConnectionDetails with { ServerPassword = null } 256 | }; 257 | return r; 258 | } 259 | 260 | public List GetMenuData(IMenuMod.MenuEntry? toggleButtonEntry) 261 | { 262 | return [ 263 | new IMenuMod.MenuEntry( 264 | "Enable Gifting", 265 | ["false", "true"], 266 | "Enable or disable interaction with the Archipelago gifting system. Requires reloading the save to take effect.", 267 | (v) => GS.EnableGifting = v == 1, 268 | () => GS.EnableGifting ? 1 : 0 269 | ) 270 | ]; 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/RM/HelperPlatformBuilder.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.HollowKnight.SlotDataModel; 2 | using ItemChanger; 3 | using System.Collections.Generic; 4 | using SD = ItemChanger.Util.SceneDataUtil; 5 | 6 | namespace Archipelago.HollowKnight.IC.RM 7 | { 8 | public static class HelperPlatformBuilder 9 | { 10 | public static IBool hasWalljump = new PDBool(nameof(PlayerData.hasWalljump)); 11 | public static IBool hasWalljumpLeft = new PDBool(nameof(ItemChanger.Modules.SplitClaw.hasWalljumpLeft)); 12 | public static IBool hasWalljumpRight = new PDBool(nameof(ItemChanger.Modules.SplitClaw.hasWalljumpRight)); 13 | public static IBool hasDoubleJump = new PDBool(nameof(PlayerData.hasDoubleJump)); 14 | 15 | public static IBool lacksLeftClaw = new Negation(new Disjunction(hasWalljump, hasWalljumpLeft)); 16 | public static IBool lacksLeftVertical = new Negation(new Disjunction(hasWalljump, hasDoubleJump, hasWalljumpLeft)); 17 | public static IBool lacksRightClaw = new Negation(new Disjunction(hasWalljump, hasWalljumpRight)); 18 | public static IBool lacksRightVertical = new Negation(new Disjunction(hasWalljump, hasDoubleJump, hasWalljumpRight)); 19 | public static IBool lacksAnyClaw = new Negation(new Disjunction(hasWalljump, hasWalljumpLeft, hasWalljumpRight)); 20 | public static IBool lacksAnyVertical = new Negation(new Disjunction(hasWalljump, hasDoubleJump, hasWalljumpLeft, hasWalljumpRight)); 21 | 22 | public static void AddStartLocationRequiredPlatforms(SlotOptions options) 23 | { 24 | string startLocationName = options.StartLocationName ?? StartLocationNames.Kings_Pass; 25 | 26 | switch (startLocationName) 27 | { 28 | // Platforms to allow escaping the Hive start regardless of difficulty or initial items 29 | case StartLocationNames.Hive: 30 | ItemChangerMod.AddDeployer(new SmallPlatform { SceneName = SceneNames.Hive_03, X = 58.5f, Y = 134f, Test = lacksRightClaw }); 31 | ItemChangerMod.AddDeployer(new SmallPlatform { SceneName = SceneNames.Hive_03, X = 58.5f, Y = 138.5f, Test = lacksAnyVertical }); 32 | break; 33 | 34 | // Drop the vine platforms and add small platforms for jumping up. 35 | case StartLocationNames.Far_Greenpath: 36 | ItemChangerMod.AddDeployer(new SmallPlatform { SceneName = SceneNames.Fungus1_13, X = 45f, Y = 16.5f, Test = lacksLeftClaw }); 37 | ItemChangerMod.AddDeployer(new SmallPlatform { SceneName = SceneNames.Fungus1_13, X = 64f, Y = 16.5f, Test = lacksRightClaw }); 38 | SD.Save(SceneNames.Fungus1_13, "Vine Platform (1)"); 39 | SD.Save(SceneNames.Fungus1_13, "Vine Platform (2)"); 40 | break; 41 | 42 | // With the Lower Greenpath start, getting to the rest of Greenpath requires 43 | // cutting the vine to the right of the vessel fragment. 44 | case StartLocationNames.Lower_Greenpath: 45 | if (options.RandomizeNail) 46 | { 47 | SD.Save(SceneNames.Fungus1_13, "Vine Platform"); 48 | } 49 | break; 50 | } 51 | } 52 | 53 | public static void AddConveniencePlatforms(SlotOptions options) 54 | { 55 | // FUTURE: when we support room rando, this should be updated based on transition placements. 56 | HashSet targetNames = new(); 57 | string startLocationName = options.StartLocationName ?? StartLocationNames.Kings_Pass; 58 | 59 | if (!options.ExtraPlatforms) 60 | { 61 | return; 62 | } 63 | 64 | // Platforms to climb out from basin wanderer's journal 65 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_02, X = 128.3f, Y = 7f, Test = lacksLeftClaw }); 66 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_02, X = 128.3f, Y = 11f, Test = lacksLeftClaw }); 67 | 68 | // Platforms to climb up to tram in basin from left with no items 69 | if (!targetNames.Contains($"{SceneNames.Abyss_03}[bot1]")) 70 | { 71 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_03, X = 34f, Y = 7f, Test = lacksRightVertical }); 72 | } 73 | 74 | // Platform to climb out of Abyss with only wings 75 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_06_Core, X = 88.6f, Y = 263f, Test = lacksLeftClaw }); 76 | 77 | // Platforms to climb back up from pale ore with no items 78 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 164.7f, Y = 30f, Test = lacksRightVertical }); 79 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 99.5f, Y = 12.5f, Test = lacksRightVertical }); 80 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 117.7f, Y = 18.8f, Test = lacksRightClaw }); 81 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 114.3f, Y = 23f, Test = lacksAnyVertical }); 82 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 117.7f, Y = 7f, Test = lacksAnyClaw }); 83 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 117.7f, Y = 10.8f, Test = lacksAnyClaw }); 84 | 85 | // Platforms to remove softlock with wings at simple key in basin 86 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_20, X = 26.5f, Y = 13f, Test = lacksAnyClaw }); 87 | 88 | // Platform for returning to Gorb landing 89 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Cliffs_02, X = 32.3f, Y = 27.7f, Test = lacksAnyVertical }); 90 | 91 | // Platform to return from Deepnest mimic grub room 92 | if (!targetNames.Contains($"{SceneNames.Deepnest_01b}[right2]") 93 | && !targetNames.Contains($"{SceneNames.Deepnest_02}[left1]") 94 | && !targetNames.Contains($"{SceneNames.Deepnest_02}[right1]")) 95 | { 96 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Deepnest_01b, X = 48.3f, Y = 40f, Test = lacksAnyVertical }); 97 | } 98 | 99 | // Platforms to return from the Deepnest_02 geo rocks without vertical 100 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Deepnest_02, X = 26f, Y = 12f, Test = lacksAnyClaw }); 101 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Deepnest_02, X = 26f, Y = 16f, Test = lacksAnyClaw }); 102 | 103 | // Platform to escape the Deepnest mimic room when mimics may not be present 104 | if (options.RandomizeMimics) 105 | { 106 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Deepnest_36, X = 26f, Y = 11f, Test = lacksLeftVertical }); 107 | } 108 | 109 | // Platforms to climb back up from Mantis Lords with only wings 110 | if (!targetNames.Contains($"{SceneNames.Fungus2_15}[left1]") 111 | && !targetNames.Contains($"{SceneNames.Fungus2_25}[top1]") 112 | && !targetNames.Contains($"{SceneNames.Fungus2_25}[top2]")) 113 | { 114 | for (int i = 0; i < 2; i++) 115 | { 116 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Fungus2_15, X = 48f + 2 * i, Y = 15f + 10 * i, Test = lacksRightClaw }); 117 | } 118 | } 119 | 120 | // Platforms to prevent softlock on lever on the way to love key. 121 | for (int i = 0; i < 2; i++) 122 | { 123 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Fungus3_05, X = 65.7f, Y = 11f + 4.5f * i, Test = lacksRightClaw }); 124 | } 125 | 126 | if (startLocationName != StartLocationNames.Hive) 127 | { 128 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Hive_03, X = 58.5f, Y = 134f, Test = lacksAnyVertical }); 129 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Hive_03, X = 58.5f, Y = 138.5f, Test = lacksAnyVertical }); 130 | } 131 | 132 | // Move the load in colo downward to prevent bench soft lock 133 | if (!targetNames.Contains($"{SceneNames.Room_Colosseum_02}[top2]") 134 | && !targetNames.Contains($"{SceneNames.Room_Colosseum_Spectate}[right1]")) 135 | { 136 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Room_Colosseum_02, X = 43.5f, Y = 45f, Test = lacksAnyClaw }); 137 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Room_Colosseum_02, X = 43.5f, Y = 49.5f, Test = lacksAnyClaw }); 138 | } 139 | 140 | // Platform to escape from the geo rock above Lemm 141 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Ruins1_05c, X = 26.6f, Y = 73.2f, Test = lacksAnyVertical }); 142 | 143 | // Platforms to climb back up to King's Pass with no items 144 | if (!targetNames.Contains($"{SceneNames.Town}[right1]") && startLocationName == StartLocationNames.Kings_Pass) 145 | { 146 | for (int i = 0; i < 6; i++) 147 | { 148 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Town, X = 20f - 2 * (i % 2), Y = 5f * i + 15f, Test = lacksLeftClaw }); 149 | } 150 | } 151 | 152 | // Platforms to prevent itemless softlock when checking left waterways 153 | if (!targetNames.Contains($"{SceneNames.Waterways_04}[left1]") 154 | && !targetNames.Contains($"{SceneNames.Waterways_04}[left2]") 155 | && !targetNames.Contains($"{SceneNames.Waterways_04b}[left1]") 156 | && !targetNames.Contains($"{SceneNames.Waterways_09}[left1]") 157 | && startLocationName != StartLocationNames.West_Waterways) 158 | { 159 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Waterways_04, X = 148f, Y = 23.1f, Test = lacksAnyVertical }); 160 | ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Waterways_04, X = 139f, Y = 32f, Test = lacksAnyVertical }); 161 | ItemChangerMod.AddDeployer(new SmallPlatform() 162 | { 163 | SceneName = SceneNames.Waterways_04, 164 | X = 107f, 165 | Y = 10f, 166 | Test = options.RandomizeSwim ? new PDBool("canSwim") : null 167 | }); 168 | ItemChangerMod.AddDeployer(new SmallPlatform() 169 | { 170 | SceneName = SceneNames.Waterways_04, 171 | X = 107f, 172 | Y = 15f, 173 | Test = options.RandomizeSwim ? new PDBool("canSwim") : null 174 | }); 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/Modules/ItemNetworkingModule.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.HollowKnight.IC.Tags; 2 | using Archipelago.MultiClient.Net; 3 | using Archipelago.MultiClient.Net.Exceptions; 4 | using Archipelago.MultiClient.Net.Models; 5 | using ItemChanger; 6 | using ItemChanger.Internal; 7 | using ItemChanger.Modules; 8 | using ItemChanger.Tags; 9 | using MenuChanger; 10 | using Modding; 11 | using Newtonsoft.Json; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Collections.ObjectModel; 15 | using System.Threading.Tasks; 16 | 17 | namespace Archipelago.HollowKnight.IC.Modules 18 | { 19 | /// 20 | /// Handles the sending and receiving of items from the server 21 | /// 22 | public class ItemNetworkingModule : Module 23 | { 24 | /// 25 | /// A preset GiveInfo structure that avoids creating geo and places messages in the corner. 26 | /// 27 | public static GiveInfo RemoteGiveInfo = new() 28 | { 29 | FlingType = FlingType.DirectDeposit, 30 | Callback = null, 31 | Container = Container.Unknown, 32 | MessageType = MessageType.Corner 33 | }; 34 | 35 | /// 36 | /// A preset GiveInfo structure that avoids creating geo and outputs no messages, e.g. for Start Items. 37 | /// 38 | public static GiveInfo SilentGiveInfo = new() 39 | { 40 | FlingType = FlingType.DirectDeposit, 41 | Callback = null, 42 | Container = Container.Unknown, 43 | MessageType = MessageType.None 44 | }; 45 | 46 | private ArchipelagoSession session => ArchipelagoMod.Instance.session; 47 | 48 | private bool networkErrored; 49 | private bool readyToSendReceiveChecks; 50 | 51 | [JsonProperty] 52 | private List deferredLocationChecks = []; 53 | [JsonProperty] 54 | private bool hasEverRecievedStartingGeo = false; 55 | 56 | public override void Initialize() 57 | { 58 | networkErrored = false; 59 | readyToSendReceiveChecks = false; 60 | ModHooks.HeroUpdateHook += PollForItems; 61 | On.GameManager.FinishedEnteringScene += DoInitialSyncAndStartSendReceive; 62 | } 63 | 64 | public override void Unload() 65 | { 66 | // DoInitialSyncAndStartSendReceive unsubscribes itself 67 | ModHooks.HeroUpdateHook -= PollForItems; 68 | session.Locations.CheckedLocationsUpdated -= OnLocationChecksUpdated; 69 | } 70 | 71 | public async Task SendLocationsAsync(params long[] locationIds) 72 | { 73 | if (!readyToSendReceiveChecks) 74 | { 75 | deferredLocationChecks.AddRange(locationIds); 76 | return; 77 | } 78 | 79 | if (networkErrored) 80 | { 81 | deferredLocationChecks.AddRange(locationIds); 82 | ReportDisconnect(); 83 | return; 84 | } 85 | 86 | try 87 | { 88 | await Task.Run(() => session.Locations.CompleteLocationChecks(locationIds)).TimeoutAfter(1000); 89 | } 90 | catch (Exception ex) when (ex is TimeoutException or ArchipelagoSocketClosedException) 91 | { 92 | ArchipelagoMod.Instance.LogWarn("SendLocationsAsync disconnected"); 93 | deferredLocationChecks.AddRange(locationIds); 94 | ReportDisconnect(); 95 | } 96 | catch (Exception ex) 97 | { 98 | ArchipelagoMod.Instance.LogError("Unexpected error in SendLocationsAsync"); 99 | ArchipelagoMod.Instance.LogError(ex); 100 | deferredLocationChecks.AddRange(locationIds); 101 | ReportDisconnect(); 102 | } 103 | } 104 | 105 | public void MarkLocationAsChecked(long locationId, bool silentGive) 106 | { 107 | // Called when marking a location as checked remotely (i.e. through ReceiveItem, etc.) 108 | // This also grants items at said locations. 109 | bool hadNewlyObtainedItems = false; 110 | bool hadUnobtainedItems = false; 111 | 112 | ArchipelagoMod.Instance.LogDebug($"Marking location {locationId} as checked."); 113 | if (!ArchipelagoPlacementTag.PlacementsByLocationId.TryGetValue(locationId, out AbstractPlacement pmt)) 114 | { 115 | ArchipelagoMod.Instance.LogDebug($"Could not find a placement for location {locationId}"); 116 | return; 117 | } 118 | 119 | foreach (AbstractItem item in pmt.Items) 120 | { 121 | if (!item.GetTag(out ArchipelagoItemTag tag)) 122 | { 123 | hadUnobtainedItems = true; 124 | continue; 125 | } 126 | 127 | if (item.WasEverObtained()) 128 | { 129 | continue; 130 | } 131 | 132 | if (tag.Location != locationId) 133 | { 134 | hadUnobtainedItems = true; 135 | continue; 136 | } 137 | 138 | hadNewlyObtainedItems = true; 139 | pmt.AddVisitFlag(VisitState.ObtainedAnyItem); 140 | 141 | GiveInfo giveInfo = silentGive ? SilentGiveInfo : RemoteGiveInfo; 142 | item.Give(pmt, giveInfo.Clone()); 143 | } 144 | 145 | if (hadNewlyObtainedItems && !hadUnobtainedItems) 146 | { 147 | pmt.AddVisitFlag(VisitState.Opened | VisitState.Dropped | VisitState.Accepted | 148 | VisitState.ObtainedAnyItem); 149 | } 150 | } 151 | 152 | private async void DoInitialSyncAndStartSendReceive(On.GameManager.orig_FinishedEnteringScene orig, GameManager self) 153 | { 154 | orig(self); 155 | if (!readyToSendReceiveChecks) 156 | { 157 | On.GameManager.FinishedEnteringScene -= DoInitialSyncAndStartSendReceive; 158 | if (!hasEverRecievedStartingGeo) 159 | { 160 | HeroController.instance.AddGeo(ArchipelagoMod.Instance.SlotData.Options.StartingGeo); 161 | hasEverRecievedStartingGeo = true; 162 | } 163 | readyToSendReceiveChecks = true; 164 | await Synchronize(); 165 | session.Locations.CheckedLocationsUpdated += OnLocationChecksUpdated; 166 | } 167 | } 168 | 169 | private void OnLocationChecksUpdated(ReadOnlyCollection newCheckedLocations) 170 | { 171 | foreach (long location in newCheckedLocations) 172 | { 173 | ThreadSupport.BeginInvoke(() => MarkLocationAsChecked(location, false)); 174 | } 175 | } 176 | 177 | private void PollForItems() 178 | { 179 | if (!readyToSendReceiveChecks || !session.Items.Any()) 180 | { 181 | return; 182 | } 183 | 184 | ReceiveNextItem(false); 185 | } 186 | 187 | private async Task Synchronize() 188 | { 189 | // receive from the server any items that are pending 190 | while (ReceiveNextItem(true)) { } 191 | // ensure any already-checked locations (co-op, restarting save) are marked cleared 192 | foreach (long location in session.Locations.AllLocationsChecked) 193 | { 194 | MarkLocationAsChecked(location, true); 195 | } 196 | // send out any pending items that didn't get to the network from the previous session 197 | long[] pendingLocations = deferredLocationChecks.ToArray(); 198 | deferredLocationChecks.Clear(); 199 | await SendLocationsAsync(pendingLocations); 200 | } 201 | 202 | private bool ReceiveNextItem(bool silentGive) 203 | { 204 | if (!session.Items.Any()) 205 | { 206 | return false; // No items are waiting. 207 | } 208 | 209 | ItemInfo itemInfo = session.Items.DequeueItem(); // Read the next item 210 | 211 | try 212 | { 213 | ReceiveItem(itemInfo, silentGive); 214 | } 215 | catch (Exception ex) 216 | { 217 | ArchipelagoMod.Instance.LogError($"Unexpected exception during receive for item {JsonConvert.SerializeObject(itemInfo.ToSerializable())}: {ex}"); 218 | } 219 | 220 | return true; 221 | } 222 | 223 | private void ReceiveItem(ItemInfo itemInfo, bool silentGive) 224 | { 225 | string name = itemInfo.ItemName; 226 | ArchipelagoMod.Instance.LogDebug($"Receiving item {itemInfo.ItemId} with name {name}. " + 227 | $"Slot is {itemInfo.Player}. Location is {itemInfo.LocationId} with name {itemInfo.LocationName}"); 228 | 229 | if (itemInfo.Player == ArchipelagoMod.Instance.session.Players.ActivePlayer.Slot && itemInfo.LocationId > 0) 230 | { 231 | MarkLocationAsChecked(itemInfo.LocationId, silentGive); 232 | return; 233 | } 234 | 235 | ArchipelagoRemoteItemCounterModule remoteTracker = ItemChangerMod.Modules.GetOrAdd(); 236 | bool shouldReceive = remoteTracker.ShouldReceiveServerItem(itemInfo); 237 | remoteTracker.IncrementServerCountForItem(itemInfo); 238 | if (!shouldReceive) 239 | { 240 | ArchipelagoMod.Instance.LogDebug($"Fast-forwarding past already received item {name} from {itemInfo.Player} at location {itemInfo.LocationDisplayName} ({itemInfo.LocationId})"); 241 | return; 242 | } 243 | 244 | // If we're still here, this is an item from someone else. We'll make up our own dummy placement and grant the item. 245 | AbstractItem item = Finder.GetItem(name); 246 | if (item == null) 247 | { 248 | ArchipelagoMod.Instance.LogWarn($"Could not find an item named '{name}'. " + 249 | $"This means that item {itemInfo.ItemId} was not received."); 250 | return; 251 | } 252 | ArchipelagoRemoteItemTag remoteItemTag = new(itemInfo); 253 | item.AddTag(remoteItemTag); 254 | 255 | string sender; 256 | if (itemInfo.LocationId == -1) 257 | { 258 | sender = "Cheat Console"; 259 | } 260 | else if (itemInfo.LocationId == -2) 261 | { 262 | sender = "Start"; 263 | } 264 | else if (itemInfo.Player == 0) 265 | { 266 | sender = "Archipelago"; 267 | } 268 | else 269 | { 270 | sender = session.Players.GetPlayerName(itemInfo.Player); 271 | } 272 | InteropTag recentItemsTag = item.AddTag(); 273 | recentItemsTag.Message = "RecentItems"; 274 | recentItemsTag.Properties["DisplaySource"] = sender; 275 | 276 | RemotePlacement pmt = RemotePlacement.GetOrAddSingleton(); 277 | item.Load(); 278 | pmt.Add(item); 279 | 280 | GiveInfo giveInfo = silentGive ? SilentGiveInfo : RemoteGiveInfo; 281 | item.Give(pmt, giveInfo.Clone()); 282 | } 283 | 284 | public void ReportDisconnect() 285 | { 286 | networkErrored = true; 287 | MessageController.Enqueue( 288 | null, 289 | "Error: Lost connection to Archipelago server" 290 | ); 291 | MessageController.Enqueue( 292 | null, 293 | "Reload your save to attempt to reconnect." 294 | ); 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/IC/Modules/DeathLinkModule.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.MultiClient.Net; 2 | using Archipelago.MultiClient.Net.BounceFeatures.DeathLink; 3 | using HutongGames.PlayMaker; 4 | using ItemChanger; 5 | using ItemChanger.Extensions; 6 | using ItemChanger.FsmStateActions; 7 | using Modding; 8 | using System; 9 | using System.Reflection; 10 | 11 | namespace Archipelago.HollowKnight.IC.Modules 12 | { 13 | public class DeathLinkModule : ItemChanger.Modules.Module 14 | { 15 | public const string PREVENT_SHADE_VARIABLE_NAME = "Deathlink Prevent Shade"; 16 | public const string IS_DEATHLINK_VARIABLE_NAME = "Is Deathlink Death"; 17 | private static readonly MethodInfo HeroController_CanTakeDamage = typeof(HeroController) 18 | .GetMethod("CanTakeDamage", BindingFlags.NonPublic | BindingFlags.Instance); 19 | 20 | private DeathLinkService service = null; 21 | private DeathLinkShadeHandling shadeHandling => ArchipelagoMod.Instance.SlotData.Options.DeathLinkShade; 22 | private bool breakFragileCharms => ArchipelagoMod.Instance.SlotData.Options.DeathLinkBreaksFragileCharms; 23 | private DeathLinkStatus status; 24 | private int lastDamageType; 25 | private DateTime lastDamageTime; 26 | private bool hasEditedFsm = false; 27 | 28 | private ArchipelagoSession session; 29 | 30 | private void Reset() 31 | { 32 | lastDamageType = 0; 33 | lastDamageTime = DateTime.MinValue; 34 | status = DeathLinkStatus.None; 35 | } 36 | 37 | public override void Initialize() 38 | { 39 | session = ArchipelagoMod.Instance.session; 40 | Reset(); 41 | 42 | ArchipelagoMod.Instance.LogDebug($"Enabling DeathLink support, type: {shadeHandling}"); 43 | service = ArchipelagoMod.Instance.session.CreateDeathLinkService(); 44 | service.EnableDeathLink(); 45 | service.OnDeathLinkReceived += OnDeathLinkReceived; 46 | ModHooks.HeroUpdateHook += OnHeroUpdate; 47 | On.HeroController.TakeDamage += OnTakeDamage; 48 | Events.AddFsmEdit(new FsmID("Hero Death Anim"), FsmEdit); 49 | } 50 | 51 | public override void Unload() 52 | { 53 | Reset(); 54 | 55 | if (service != null) 56 | { 57 | service.OnDeathLinkReceived -= OnDeathLinkReceived; 58 | service = null; 59 | } 60 | 61 | ModHooks.HeroUpdateHook -= OnHeroUpdate; 62 | On.HeroController.TakeDamage -= OnTakeDamage; 63 | Events.RemoveFsmEdit(new FsmID("Hero Death Anim"), FsmEdit); 64 | hasEditedFsm = false; 65 | } 66 | 67 | private void FsmEdit(PlayMakerFSM fsm) 68 | { 69 | if (hasEditedFsm) 70 | { 71 | return; 72 | } 73 | hasEditedFsm = true; 74 | 75 | ArchipelagoMod ap = ArchipelagoMod.Instance; 76 | 77 | FsmBool preventShade = fsm.AddFsmBool(PREVENT_SHADE_VARIABLE_NAME, false); 78 | FsmBool isDeathlink = fsm.AddFsmBool(IS_DEATHLINK_VARIABLE_NAME, false); 79 | // Death animation starts here - normally whether you get a shade or not is determined purely by whether 80 | // you're in a dream or not. 81 | FsmState mapZone = fsm.GetState("Map Zone"); 82 | 83 | // If it's not someone else's death, then send out a deathlink. Also compute whether a shade should be spawned since 84 | // multiple other states need to know. 85 | mapZone.AddFirstAction(new Lambda(() => 86 | { 87 | ap.LogDebug($"FsmEdit Pre: Status={status} Shade handling={shadeHandling} Break fragiles={breakFragileCharms}."); 88 | 89 | bool isDeathlinkDeath = status == DeathLinkStatus.Dying; 90 | 91 | if (!isDeathlinkDeath) 92 | { 93 | ap.LogDebug($"FsmEdit Pre: Not a deathlink death, so sending out our own deathlink."); 94 | // If we're not caused by DeathLink... then we send a DeathLink 95 | SendDeathLink(); 96 | return; 97 | } 98 | else 99 | { 100 | ap.LogDebug("Beginning deathlink death handling"); 101 | } 102 | 103 | isDeathlink.Value = isDeathlinkDeath; 104 | preventShade.Value = !( 105 | shadeHandling == DeathLinkShadeHandling.Vanilla 106 | || shadeHandling == DeathLinkShadeHandling.Shade && PlayerData.instance.shadeScene == "None" 107 | ); 108 | 109 | if (!preventShade.Value) 110 | { 111 | ap.LogDebug($"FsmEdit Pre: Shade will be created."); 112 | } 113 | })); 114 | 115 | // route around penalties based on settings 116 | FsmState breakMsg = fsm.GetState("Break Msg"); 117 | FsmState removeOvercharm = fsm.GetState("Remove Overcharm"); 118 | 119 | FsmState createShadeCheck = fsm.AddState("Create Shade?"); 120 | createShadeCheck.AddLastAction(new DelegateBoolTest(() => isDeathlink.Value, null, "FINISHED")); 121 | createShadeCheck.AddLastAction(new DelegateBoolTest(() => preventShade.Value, "SKIP SHADE", null)); 122 | createShadeCheck.AddTransition("SKIP SHADE", "Save"); 123 | createShadeCheck.AddTransition("FINISHED", "Remove Geo"); 124 | 125 | FsmState breakFragilesCheck = fsm.AddState("Break Fragiles?"); 126 | breakFragilesCheck.AddLastAction(new DelegateBoolTest(() => isDeathlink.Value, null, "FINISHED")); 127 | breakFragilesCheck.AddLastAction(new DelegateBoolTest(() => !breakFragileCharms, "SKIP BREAK", null)); 128 | breakFragilesCheck.AddTransition("SKIP BREAK", createShadeCheck); 129 | breakFragilesCheck.AddTransition("FINISHED", "Break Glass HP"); 130 | 131 | mapZone.RemoveTransitionsOn("FINISHED"); 132 | mapZone.AddTransition("FINISHED", breakFragilesCheck); 133 | 134 | breakMsg.RemoveTransitionsOn("FINISHED"); 135 | breakMsg.AddTransition("FINISHED", createShadeCheck); 136 | removeOvercharm.RemoveTransitionsOn("FINISHED"); 137 | removeOvercharm.AddTransition("FINISHED", createShadeCheck); 138 | 139 | // adjust soul limiter to be created only if a shade was created 140 | FsmState deathEnding = fsm.GetState("End"); 141 | fsm.GetState("Limit Soul?").Actions = []; 142 | // Replace the first two action (which normally start the soul limiter and notify about it) 143 | deathEnding.Actions[0] = new Lambda(() => 144 | { 145 | // Mimic the Limit Soul? state and the action being replaced - we only want to soul limit if the 146 | // player spawned a shade 147 | if (!preventShade.Value) 148 | { 149 | fsm.Fsm.BroadcastEvent("SOUL LIMITER UP"); 150 | GameManager.instance.StartSoulLimiter(); 151 | } 152 | }); 153 | 154 | // the following 3 states are the ending states of each branch of the FSM. we'll link them into a custom state that resets 155 | // deathlink for us 156 | FsmState dreamReturn = fsm.GetState("Dream Return"); 157 | FsmState waitForHeroController = fsm.GetState("Wait for HeroController"); 158 | FsmState steelSoulCheck = fsm.GetState("Shade?"); 159 | FsmState[] endingStates = [dreamReturn, waitForHeroController, steelSoulCheck]; 160 | // add deathlink cleanup state 161 | FsmState cleanupDeathlink = fsm.AddState("Cleanup Deathlink"); 162 | cleanupDeathlink.AddFirstAction(new Lambda(() => 163 | { 164 | ap.LogDebug("Resetting deathlink state"); 165 | preventShade.Value = false; 166 | isDeathlink.Value = false; 167 | status = DeathLinkStatus.None; 168 | })); 169 | foreach (FsmState state in endingStates) 170 | { 171 | state.AddTransition("FINISHED", cleanupDeathlink); 172 | } 173 | } 174 | 175 | public void MurderPlayer() 176 | { 177 | string scene = GameManager.instance.sceneName; 178 | ArchipelagoMod.Instance.LogDebug($"Deathlink-initiated kill starting. Current scene: {scene}"); 179 | status = DeathLinkStatus.Dying; 180 | HeroController.instance.TakeDamage(HeroController.instance.gameObject, GlobalEnums.CollisionSide.other, 181 | 9999, 0); 182 | } 183 | 184 | private void OnHeroUpdate() 185 | { 186 | HeroController hc = HeroController.instance; 187 | if (status == DeathLinkStatus.Pending 188 | && hc.acceptingInput 189 | && hc.damageMode == GlobalEnums.DamageMode.FULL_DAMAGE 190 | && PlayerData.instance.GetInt(nameof(PlayerData.health)) > 0 191 | && (bool)HeroController_CanTakeDamage.Invoke(hc, null)) 192 | { 193 | MurderPlayer(); 194 | } 195 | } 196 | 197 | private void OnTakeDamage(On.HeroController.orig_TakeDamage orig, HeroController self, 198 | UnityEngine.GameObject go, GlobalEnums.CollisionSide damageSide, int damageAmount, int hazardType) 199 | { 200 | lastDamageTime = DateTime.UtcNow; 201 | lastDamageType = hazardType; 202 | orig(self, go, damageSide, damageAmount, hazardType); 203 | } 204 | 205 | public void SendDeathLink() 206 | { 207 | ArchipelagoMod ap = ArchipelagoMod.Instance; 208 | // Don't send death links if we're currently in the process of dying to another deathlink. 209 | if (status == DeathLinkStatus.Dying) 210 | { 211 | ap.LogDebug("SendDeathLink(): Not sending a deathlink because we're in the process of dying to one"); 212 | return; 213 | } 214 | 215 | if (service == null) 216 | { 217 | ap.LogDebug("SendDeathLink(): Not sending a deathlink because not enabled."); 218 | return; 219 | } 220 | 221 | if ((DateTime.UtcNow - lastDamageTime).TotalSeconds > 5) 222 | { 223 | ap.LogWarn("Last damage was a long time ago, resetting damage type to zero."); 224 | // Damage source was more than 5 seconds ago, so ignore damage type 225 | lastDamageType = 0; 226 | } 227 | 228 | string message = DeathLinkMessages.GetDeathMessage(lastDamageType, ArchipelagoMod.Instance.session.Players.ActivePlayer.Alias); 229 | ap.LogDebug( 230 | $"SendDeathLink(): Sending deathlink. \"{message}\""); 231 | service.SendDeathLink(new DeathLink(ArchipelagoMod.Instance.session.Players.ActivePlayer.Alias, message)); 232 | } 233 | 234 | private void OnDeathLinkReceived(DeathLink deathLink) 235 | { 236 | ArchipelagoMod ap = ArchipelagoMod.Instance; 237 | ap.LogDebug($"OnDeathLinkReceived(): Receiving deathlink. Status={status}."); 238 | 239 | if (status == DeathLinkStatus.None) 240 | { 241 | status = DeathLinkStatus.Pending; 242 | 243 | string cause = deathLink.Cause; 244 | if (cause == null || cause == "") 245 | { 246 | cause = $"{deathLink.Source} died."; 247 | } 248 | 249 | MenuChanger.ThreadSupport.BeginInvoke(() => 250 | { 251 | new ItemChanger.UIDefs.MsgUIDef() 252 | { 253 | name = new BoxedString(cause), 254 | sprite = new ArchipelagoSprite { key = "DeathLinkIcon" } 255 | }.SendMessage(MessageType.Corner, null); 256 | }); 257 | 258 | lastDamageType = 0; 259 | } 260 | else 261 | { 262 | ap.LogDebug("Skipping this deathlink as one is currently in progress"); 263 | } 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Goals.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.HollowKnight.IC; 2 | using Archipelago.HollowKnight.IC.Items; 3 | using Archipelago.HollowKnight.IC.Modules; 4 | using Archipelago.MultiClient.Net.Exceptions; 5 | using ItemChanger; 6 | using ItemChanger.Extensions; 7 | using ItemChanger.Internal; 8 | using ItemChanger.Placements; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | 14 | namespace Archipelago.HollowKnight 15 | { 16 | public enum GoalsLookup 17 | { 18 | Any = 0, 19 | HollowKnight = 1, 20 | SealedSiblings = 2, 21 | Radiance = 3, 22 | Godhome = 4, 23 | GodhomeFlower = 5, 24 | GrubHunt = 6, 25 | MAX = GrubHunt 26 | } 27 | 28 | public abstract class Goal 29 | { 30 | public abstract string Name { get; } 31 | public abstract string Description { get; } 32 | public virtual bool CanBeSelectedForAnyGoal => true; 33 | 34 | private static readonly Dictionary Lookup = new() 35 | { 36 | [GoalsLookup.HollowKnight] = new HollowKnightGoal(), 37 | [GoalsLookup.SealedSiblings] = new SealedSiblingsGoal(), 38 | [GoalsLookup.Radiance] = new RadianceGoal(), 39 | [GoalsLookup.Godhome] = new GodhomeGoal(), 40 | [GoalsLookup.GodhomeFlower] = new GodhomeFlowerGoal(), 41 | [GoalsLookup.GrubHunt] = new GrubHuntGoal(), 42 | }; 43 | 44 | static Goal() 45 | { 46 | Lookup[GoalsLookup.Any] = new AnyGoal(Lookup.Values.ToList()); 47 | } 48 | 49 | protected void FountainPlaqueTopEdit(ref string s) => s = "Your goal is"; 50 | protected void FountainPlaqueNameEdit(ref string s) => s = Name; 51 | protected void FountainPlaqueDescEdit(ref string s) => s = Description; 52 | 53 | protected abstract bool VictoryCondition(); 54 | 55 | public static Goal GetGoal(GoalsLookup key) 56 | { 57 | Goal value; 58 | if (Lookup.TryGetValue(key, out value)) 59 | { 60 | return value; 61 | } 62 | ArchipelagoMod.Instance.LogError($"Listed goal is {key}, which is greater than {GoalsLookup.MAX}. Is this an outdated client?"); 63 | throw new ArgumentOutOfRangeException($"Unrecognized goal condition {key} (are you running an outdated client?)"); 64 | } 65 | 66 | public async Task CheckForVictoryAsync() 67 | { 68 | ArchipelagoMod.Instance.LogDebug($"Checking for victory; goal is {this.Name}; scene " + 69 | $"{UnityEngine.SceneManagement.SceneManager.GetActiveScene().name}"); 70 | if (VictoryCondition()) 71 | { 72 | ArchipelagoMod.Instance.LogDebug($"Victory detected, declaring!"); 73 | try 74 | { 75 | await ItemChangerMod.Modules.Get().DeclareVictoryAsync().TimeoutAfter(1000); 76 | } 77 | catch (Exception ex) when (ex is TimeoutException or ArchipelagoSocketClosedException) 78 | { 79 | ArchipelagoMod.Instance.LogError("Failed to send goal to server"); 80 | ArchipelagoMod.Instance.LogError(ex); 81 | } 82 | } 83 | } 84 | 85 | public void Select() 86 | { 87 | Events.AddLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_TOP"), FountainPlaqueTopEdit); 88 | Events.AddLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_MAIN"), FountainPlaqueNameEdit); 89 | Events.AddLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_DESC"), FountainPlaqueDescEdit); 90 | OnSelected(); 91 | } 92 | 93 | public void Deselect() 94 | { 95 | Events.RemoveLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_TOP"), FountainPlaqueTopEdit); 96 | Events.RemoveLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_MAIN"), FountainPlaqueNameEdit); 97 | Events.RemoveLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_DESC"), FountainPlaqueDescEdit); 98 | OnDeselected(); 99 | } 100 | 101 | public abstract void OnSelected(); 102 | public abstract void OnDeselected(); 103 | } 104 | 105 | public class AnyGoal : Goal 106 | { 107 | private IReadOnlyList subgoals; 108 | 109 | public override string Name => "Any Goal"; 110 | 111 | public override string Description => "Do whichever goal you like. If you're not sure,
try defeating the Hollow Knight!"; 112 | 113 | public AnyGoal(IReadOnlyList subgoals) 114 | { 115 | this.subgoals = subgoals; 116 | } 117 | 118 | public override void OnSelected() 119 | { 120 | foreach (Goal goal in subgoals.Where(g => g.CanBeSelectedForAnyGoal)) 121 | { 122 | goal.OnSelected(); 123 | } 124 | } 125 | 126 | public override void OnDeselected() 127 | { 128 | foreach (Goal goal in subgoals.Where(g => g.CanBeSelectedForAnyGoal)) 129 | { 130 | goal.OnDeselected(); 131 | } 132 | } 133 | 134 | protected override bool VictoryCondition() 135 | { 136 | // this goal is never completed on its own, it relies on subgoals to check for victory themselves. 137 | throw new NotImplementedException(); 138 | } 139 | } 140 | 141 | /// 142 | /// A goal which is achieved by completing a given ending (or harder) 143 | /// 144 | public abstract class EndingGoal : Goal 145 | { 146 | private static List VictoryScenes = new() 147 | { 148 | SceneNames.Cinematic_Ending_A, // THK 149 | SceneNames.Cinematic_Ending_B, // Sealed Siblings 150 | SceneNames.Cinematic_Ending_C, // Radiance 151 | "Cinematic_Ending_D", // Godhome no flower quest 152 | SceneNames.Cinematic_Ending_E // Godhome w/ flower quest 153 | }; 154 | 155 | public abstract string MinimumGoalScene { get; } 156 | 157 | public override void OnSelected() 158 | { 159 | Events.OnSceneChange += SceneChanged; 160 | } 161 | 162 | public override void OnDeselected() 163 | { 164 | Events.OnSceneChange -= SceneChanged; 165 | } 166 | 167 | private async void SceneChanged(UnityEngine.SceneManagement.Scene obj) 168 | { 169 | await CheckForVictoryAsync(); 170 | } 171 | 172 | protected override bool VictoryCondition() 173 | { 174 | string activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name; 175 | if (activeScene.StartsWith("Cinematic_Ending_")) 176 | { 177 | int minGoalSceneIndex = VictoryScenes.IndexOf(MinimumGoalScene); 178 | int sceneIndex = VictoryScenes.IndexOf(activeScene); 179 | return sceneIndex >= minGoalSceneIndex; 180 | } 181 | return false; 182 | } 183 | 184 | } 185 | 186 | /// 187 | /// A goal which is achieved by obtaining the Victory item placed at a given location in the world 188 | /// 189 | public abstract class ItemGoal : Goal 190 | { 191 | // We don't use CheckForVictoryAsync here, rather, the Victory item uses GoalModule.DeclareVictoryAsync 192 | // directly when acquired. 193 | protected override bool VictoryCondition() => false; 194 | 195 | protected virtual string GetGoalItemName() => "Victory"; 196 | protected abstract string GetGoalLocation(); 197 | protected virtual Cost GetGoalCost() => null; 198 | protected virtual UIDef GetGoalUIDef() => new ArchipelagoUIDef() 199 | { 200 | name = new BoxedString(GetGoalItemName()), 201 | shopDesc = new BoxedString("You completed your goal so you should probably get this to flex on your friends."), 202 | sprite = new ArchipelagoSprite { key = "IconColorSmall" } 203 | }; 204 | 205 | public override void OnSelected() 206 | { 207 | string goalLocation = GetGoalLocation(); 208 | AbstractPlacement plt = Ref.Settings.Placements.GetOrDefault(goalLocation); 209 | if (plt == null) 210 | { 211 | plt = Finder.GetLocation(goalLocation).Wrap(); 212 | } 213 | 214 | // don't duplicate the goal 215 | if (plt.Items.Any(i => i is GoalItem)) 216 | { 217 | return; 218 | } 219 | 220 | AbstractItem item = new GoalItem() 221 | { 222 | name = GetGoalItemName(), 223 | UIDef = GetGoalUIDef(), 224 | }; 225 | // modules (and therefore goals) are loaded prior to placements so nothing special needed 226 | // to make this load. 227 | plt.Add(item); 228 | 229 | // handle the cost 230 | if (plt is ISingleCostPlacement icsp) 231 | { 232 | Cost desiredCost = GetGoalCost(); 233 | if (icsp.Cost == null) 234 | { 235 | icsp.Cost = desiredCost; 236 | } 237 | else 238 | { 239 | icsp.Cost += desiredCost; 240 | } 241 | } 242 | else 243 | { 244 | item.AddTag(new CostTag() 245 | { 246 | Cost = GetGoalCost(), 247 | }); 248 | } 249 | } 250 | 251 | public override void OnDeselected() { } 252 | } 253 | 254 | public class HollowKnightGoal : EndingGoal 255 | { 256 | public override string Name => "The Hollow Knight"; 257 | public override string Description => "Defeat The Hollow Knight
or any harder ending."; 258 | public override string MinimumGoalScene => SceneNames.Cinematic_Ending_A; 259 | } 260 | 261 | public class SealedSiblingsGoal : EndingGoal 262 | { 263 | public override string Name => "Sealed Siblings"; 264 | public override string Description => "Complete the Sealed Siblings ending
or any harder ending."; 265 | public override string MinimumGoalScene => SceneNames.Cinematic_Ending_B; 266 | } 267 | 268 | public class RadianceGoal : EndingGoal 269 | { 270 | public override string Name => "Dream No More"; 271 | public override string Description => "Defeat The Radiance in Black Egg Temple
or Absolute Radiance in Pantheon 5."; 272 | public override string MinimumGoalScene => SceneNames.Cinematic_Ending_C; 273 | } 274 | 275 | public class GodhomeGoal : EndingGoal 276 | { 277 | public override string Name => "Embrace the Void"; 278 | public override string Description => "Defeat Absolute Radiance in Pantheon 5."; 279 | public override string MinimumGoalScene => "Cinematic_Ending_D"; 280 | } 281 | 282 | public class GodhomeFlowerGoal : EndingGoal 283 | { 284 | public override string Name => "Delicate Flower"; 285 | public override string Description => "Defeat Absolute Radiance in Pantheon 5
after delivering the flower to the Godseeker."; 286 | public override string MinimumGoalScene => SceneNames.Cinematic_Ending_E; 287 | } 288 | 289 | public class GrubHuntGoal : ItemGoal 290 | { 291 | public override string Name => "Grub Hunt"; 292 | 293 | public override string Description => $"Save {ArchipelagoMod.Instance.SlotData.GrubsRequired.Value} of your Grubs and visit Grubfather
to obtain happiness."; 294 | 295 | public override bool CanBeSelectedForAnyGoal => ArchipelagoMod.Instance.SlotData.GrubsRequired != null; 296 | 297 | protected override string GetGoalItemName() => "Happiness"; 298 | protected override string GetGoalLocation() => LocationNames.Grubfather; 299 | protected override Cost GetGoalCost() => Cost.NewGrubCost(ArchipelagoMod.Instance.SlotData.GrubsRequired.Value); 300 | protected override UIDef GetGoalUIDef() => new ArchipelagoUIDef() 301 | { 302 | name = new BoxedString("Happiness"), 303 | shopDesc = new BoxedString("Meemawmaw! Meemawmaw!"), 304 | sprite = new ArchipelagoSprite { key = "GrubHappyv2" } 305 | }; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /Archipelago.HollowKnight/ArchipelagoRandomizer.cs: -------------------------------------------------------------------------------- 1 | using Archipelago.HollowKnight.IC; 2 | using Archipelago.HollowKnight.IC.Modules; 3 | using Archipelago.HollowKnight.IC.RM; 4 | using Archipelago.HollowKnight.SlotDataModel; 5 | using Archipelago.MultiClient.Net; 6 | using Archipelago.MultiClient.Net.Models; 7 | using ItemChanger; 8 | using ItemChanger.Extensions; 9 | using ItemChanger.Modules; 10 | using ItemChanger.Placements; 11 | using ItemChanger.Tags; 12 | using Newtonsoft.Json; 13 | using System; 14 | using System.Collections.Generic; 15 | using System.Linq; 16 | using System.Threading.Tasks; 17 | 18 | namespace Archipelago.HollowKnight 19 | { 20 | /// 21 | /// Tracks state only required during initial randomization 22 | /// 23 | internal class ArchipelagoRandomizer 24 | { 25 | /// 26 | /// Randomized charm notch costs as stored in slot data. 27 | /// 28 | public List NotchCosts { get; private set; } 29 | 30 | /// 31 | /// Tracks created placements and their associated locations during randomization. 32 | /// 33 | public Dictionary placements = new(); 34 | 35 | /// 36 | /// Seeded RNG for clientside randomization. 37 | /// 38 | public readonly Random Random; 39 | 40 | /// 41 | /// Factory for IC item creation 42 | /// 43 | public readonly ItemFactory itemFactory; 44 | 45 | /// 46 | /// Factory for IC cost creation 47 | /// 48 | public readonly CostFactory costFactory; 49 | 50 | private readonly SlotData SlotData; 51 | private ArchipelagoSession Session => ArchipelagoMod.Instance.session; 52 | private ArchipelagoMod Instance => ArchipelagoMod.Instance; 53 | 54 | public ArchipelagoRandomizer(SlotData slotData) 55 | { 56 | SlotData = slotData; 57 | Random = new Random(slotData.Seed); 58 | itemFactory = new ItemFactory(); 59 | costFactory = new CostFactory(slotData.LocationCosts); 60 | NotchCosts = slotData.NotchCosts; 61 | 62 | ArchipelagoMod.Instance.Log("Initializing ArchipelagoRandomizer with slot data: " + JsonConvert.SerializeObject(SlotData)); 63 | } 64 | 65 | public void Randomize() 66 | { 67 | ArchipelagoSession session = Session; 68 | ItemChangerMod.CreateSettingsProfile(); 69 | if (SlotData.Options.StartLocationName is string start) 70 | { 71 | if (IC.RM.StartDef.Lookup.TryGetValue(start, out IC.RM.StartDef def)) 72 | { 73 | ItemChangerMod.ChangeStartGame(def.ToItemChangerStartDef()); 74 | ArchipelagoMod.Instance.Log($"Set start to {start}"); 75 | } 76 | else 77 | { 78 | ArchipelagoMod.Instance.LogError($"Unsupported start location {start}, starting in King's Pass"); 79 | } 80 | } 81 | 82 | // Add IC modules as needed 83 | // FUTURE: If Entrance rando, disable palace midwarp and some logical blockers 84 | // if (Entrance Rando Is Enabled) { 85 | // ItemChangerMod.Modules.Add(); 86 | // ItemChangerMod.Modules.Add(); 87 | // } 88 | 89 | AddItemChangerModules(); 90 | AddHelperPlatforms(); 91 | 92 | ApplyCharmCosts(); 93 | 94 | // Initialize shop locations in case they end up with zero items placed. 95 | AbstractLocation location; 96 | AbstractPlacement pmt; 97 | 98 | string[] shops = [ 99 | LocationNames.Sly, LocationNames.Sly_Key, LocationNames.Iselda, 100 | LocationNames.Salubra, LocationNames.Leg_Eater, LocationNames.Grubfather, 101 | LocationNames.Seer 102 | ]; 103 | foreach (string name in shops) 104 | { 105 | location = Finder.GetLocation(name); 106 | placements[name] = pmt = location.Wrap(); 107 | 108 | pmt.AddTag(); 109 | 110 | if (pmt is ShopPlacement shop) 111 | { 112 | shop.defaultShopItems = DefaultShopItems.IseldaMapPins 113 | | DefaultShopItems.IseldaMapMarkers 114 | | DefaultShopItems.LegEaterRepair; 115 | if (SlotData.Options.AddUnshuffledLocations) 116 | { 117 | // AP will add the default items on our behalf 118 | continue; 119 | } 120 | 121 | if (!SlotData.Options.RandomizeCharms) 122 | { 123 | shop.defaultShopItems |= DefaultShopItems.SlyCharms 124 | | DefaultShopItems.SlyKeyCharms 125 | | DefaultShopItems.IseldaCharms 126 | | DefaultShopItems.SalubraCharms 127 | | DefaultShopItems.LegEaterCharms; 128 | } 129 | if (!SlotData.Options.RandomizeMaps) 130 | { 131 | shop.defaultShopItems |= DefaultShopItems.IseldaMaps 132 | | DefaultShopItems.IseldaQuill; 133 | } 134 | if (!SlotData.Options.RandomizeCharmNotches) 135 | { 136 | shop.defaultShopItems |= DefaultShopItems.SalubraNotches 137 | | DefaultShopItems.SalubraBlessing; 138 | } 139 | if (!SlotData.Options.RandomizeKeys) 140 | { 141 | shop.defaultShopItems |= DefaultShopItems.SlySimpleKey 142 | | DefaultShopItems.SlyLantern 143 | | DefaultShopItems.SlyKeyElegantKey; 144 | } 145 | if (!SlotData.Options.RandomizeMaskShards) 146 | { 147 | shop.defaultShopItems |= DefaultShopItems.SlyMaskShards; 148 | } 149 | if (!SlotData.Options.RandomizeVesselFragments) 150 | { 151 | shop.defaultShopItems |= DefaultShopItems.SlyVesselFragments; 152 | } 153 | if (!SlotData.Options.RandomizeRancidEggs) 154 | { 155 | shop.defaultShopItems |= DefaultShopItems.SlyRancidEgg; 156 | } 157 | } 158 | else if (name == LocationNames.Grubfather) 159 | { 160 | DestroyGrubRewardTag t = pmt.AddTag(); 161 | t.destroyRewards = GrubfatherRewards.None; 162 | if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeMaskShards) 163 | { 164 | t.destroyRewards |= GrubfatherRewards.MaskShard; 165 | } 166 | if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeCharms) 167 | { 168 | t.destroyRewards |= GrubfatherRewards.Grubsong | GrubfatherRewards.GrubberflysElegy; 169 | } 170 | if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeRancidEggs) 171 | { 172 | t.destroyRewards |= GrubfatherRewards.RancidEgg; 173 | } 174 | if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeRelics) 175 | { 176 | t.destroyRewards |= GrubfatherRewards.HallownestSeal | GrubfatherRewards.KingsIdol; 177 | } 178 | if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizePaleOre) 179 | { 180 | t.destroyRewards |= GrubfatherRewards.PaleOre; 181 | } 182 | } 183 | else if (name == LocationNames.Seer) 184 | { 185 | DestroySeerRewardTag t = pmt.AddTag(); 186 | t.destroyRewards = SeerRewards.None; 187 | if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeRelics) 188 | { 189 | t.destroyRewards |= SeerRewards.HallownestSeal | SeerRewards.ArcaneEgg; 190 | } 191 | if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizePaleOre) 192 | { 193 | t.destroyRewards |= SeerRewards.PaleOre; 194 | } 195 | if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeCharms) 196 | { 197 | t.destroyRewards |= SeerRewards.DreamWielder; 198 | } 199 | if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeVesselFragments) 200 | { 201 | t.destroyRewards |= SeerRewards.VesselFragment; 202 | } 203 | if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeSkills) 204 | { 205 | t.destroyRewards |= SeerRewards.DreamGate | SeerRewards.AwokenDreamNail; 206 | } 207 | if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeMaskShards) 208 | { 209 | t.destroyRewards |= SeerRewards.MaskShard; 210 | } 211 | } 212 | } 213 | 214 | Task> scoutTask = session.Locations 215 | .ScoutLocationsAsync(session.Locations.AllLocations.ToArray()); 216 | scoutTask.Wait(); 217 | 218 | Dictionary scoutResult = scoutTask.Result; 219 | foreach (KeyValuePair scout in scoutResult) 220 | { 221 | long id = scout.Key; 222 | ScoutedItemInfo item = scout.Value; 223 | string itemName = item.ItemName ?? $"?Item {item.ItemId}"; 224 | PlaceItem(item.LocationName, itemName, item); 225 | } 226 | ItemChangerMod.AddPlacements(placements.Values); 227 | 228 | } 229 | 230 | private void AddItemChangerModules() 231 | { 232 | ItemChangerMod.Modules.Add(); 233 | ItemChangerMod.Modules.Add(); 234 | ItemChangerMod.Modules.Add(); 235 | ItemChangerMod.Modules.Add(); 236 | ItemChangerMod.Modules.Add(); 237 | ItemChangerMod.Modules.Add(); 238 | ItemChangerMod.Modules.Add(); 239 | ItemChangerMod.Modules.Add(); 240 | ItemChangerMod.Modules.Add(); 241 | 242 | if (SlotData.Options.DeathLink) 243 | { 244 | ItemChangerMod.Modules.Add(); 245 | } 246 | 247 | if (SlotData.Options.RandomizeElevatorPass) 248 | { 249 | ItemChangerMod.Modules.Add(); 250 | } 251 | 252 | if (SlotData.Options.RandomizeFocus) 253 | { 254 | ItemChangerMod.Modules.Add(); 255 | } 256 | 257 | if (SlotData.Options.RandomizeSwim) 258 | { 259 | ItemChangerMod.Modules.Add(); 260 | } 261 | 262 | if (SlotData.Options.SplitMothwingCloak) 263 | { 264 | ItemChangerMod.Modules.Add(); 265 | } 266 | 267 | if (SlotData.Options.SplitMantisClaw) 268 | { 269 | ItemChangerMod.Modules.Add(); 270 | } 271 | 272 | if (SlotData.Options.SplitCrystalHeart) 273 | { 274 | ItemChangerMod.Modules.Add(); 275 | } 276 | 277 | if (SlotData.Options.Slopeballs) 278 | { 279 | ItemChangerMod.Modules.Add(); 280 | } 281 | } 282 | 283 | private void AddHelperPlatforms() 284 | { 285 | HelperPlatformBuilder.AddConveniencePlatforms(SlotData.Options); 286 | HelperPlatformBuilder.AddStartLocationRequiredPlatforms(SlotData.Options); 287 | } 288 | 289 | private void ApplyCharmCosts() 290 | { 291 | bool isNotchCostsRandomizedOrPlando = false; 292 | for (int i = 0; i < NotchCosts.Count; i++) 293 | { 294 | if (PlayerData.instance.GetInt($"charmCost_{i + 1}") != NotchCosts[i]) 295 | { 296 | isNotchCostsRandomizedOrPlando = true; 297 | break; 298 | } 299 | } 300 | if (!isNotchCostsRandomizedOrPlando) 301 | { 302 | return; 303 | } 304 | 305 | ItemChangerMod.Modules.Add(); 306 | ItemChangerMod.Modules.Add(); 307 | PlayerDataEditModule playerDataEditModule = ItemChangerMod.Modules.GetOrAdd(); 308 | Instance.LogDebug(playerDataEditModule); 309 | for (int i = 0; i < NotchCosts.Count; i++) 310 | { 311 | playerDataEditModule.AddPDEdit($"charmCost_{i + 1}", NotchCosts[i]); 312 | } 313 | } 314 | 315 | public void PlaceItem(string location, string name, ScoutedItemInfo itemInfo) 316 | { 317 | Instance.LogDebug($"[PlaceItem] Placing item {name} into {location} with ID {itemInfo.ItemId}"); 318 | 319 | string originalLocation = string.Copy(location); 320 | location = StripShopSuffix(location); 321 | // IC does not like placements at these locations if there's also a location at the lore tablet, it renders the lore tablet inoperable. 322 | // But we can have multiple placements at the same location, so do this workaround. (Rando4 does something similar per its README) 323 | if (SlotData.Options.RandomizeLoreTablets) 324 | { 325 | switch (location) 326 | { 327 | case LocationNames.Focus: 328 | location = LocationNames.Lore_Tablet_Kings_Pass_Focus; 329 | break; 330 | case LocationNames.World_Sense: 331 | location = LocationNames.Lore_Tablet_World_Sense; 332 | break; 333 | // no default 334 | } 335 | } 336 | 337 | AbstractLocation loc = Finder.GetLocation(location); 338 | if (loc == null) 339 | { 340 | Instance.LogDebug($"[PlaceItem] Location was null: Name: {location}."); 341 | return; 342 | } 343 | 344 | bool isMyItem = itemInfo.IsReceiverRelatedToActivePlayer; 345 | string recipientName = null; 346 | if (!isMyItem) 347 | { 348 | recipientName = Session.Players.GetPlayerName(itemInfo.Player); 349 | } 350 | 351 | AbstractPlacement pmt = placements.GetOrDefault(location); 352 | if (pmt == null) 353 | { 354 | pmt = loc.Wrap(); 355 | pmt.AddTag(); 356 | placements[location] = pmt; 357 | } 358 | 359 | AbstractItem item; 360 | if (isMyItem) 361 | { 362 | item = itemFactory.CreateMyItem(name, itemInfo); 363 | } 364 | else 365 | { 366 | item = itemFactory.CreateRemoteItem(pmt, recipientName, name, itemInfo); 367 | } 368 | 369 | pmt.Add(item); 370 | costFactory.ApplyCost(pmt, item, originalLocation); 371 | } 372 | 373 | private string StripShopSuffix(string location) 374 | { 375 | if (string.IsNullOrEmpty(location)) 376 | { 377 | return null; 378 | } 379 | 380 | string[] names = 381 | [ 382 | LocationNames.Sly_Key, LocationNames.Sly, LocationNames.Iselda, LocationNames.Salubra, 383 | LocationNames.Leg_Eater, LocationNames.Egg_Shop, LocationNames.Seer, LocationNames.Grubfather 384 | ]; 385 | 386 | foreach (string name in names) 387 | { 388 | if (location.StartsWith(name)) 389 | { 390 | return location.Substring(0, name.Length); 391 | } 392 | } 393 | 394 | return location; 395 | } 396 | } 397 | } -------------------------------------------------------------------------------- /Archipelago.HollowKnight/Archipelago.HollowKnight.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Archipelago.HollowKnight 4 | Archipelago.HollowKnight 5 | net472 6 | HollowKnight.Archipelago 7 | HollowKnight.Archipelago 8 | The Archipelago Multiworld client for Hollow Knight 9 | 10 | 11 | 0.11.0 12 | bin\$(Configuration)\ 13 | preview 14 | The Archipelago Community 15 | https://github.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight 16 | git 17 | MIT 18 | Archipelago 19 | ../API 20 | bin/Publish 21 | Hussein Farran, Daniel Grace, BadMagic, KonoTyran, Dragonglove 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | all 54 | runtime; build; native; contentfiles; analyzers; buildtransitive 55 | 56 | 57 | 58 | all 59 | runtime; build; native; contentfiles; analyzers; buildtransitive 60 | 61 | 62 | all 63 | runtime; build; native; contentfiles; analyzers; buildtransitive 64 | 65 | 66 | 67 | 68 | $(HollowKnightRefs)/Assembly-CSharp.dll 69 | 70 | 71 | $(HollowKnightRefs)/Assembly-CSharp-firstpass.dll 72 | 73 | 74 | $(HollowKnightRefs)/Mods/Benchwarp/Benchwarp.dll 75 | 76 | 77 | $(HollowKnightRefs)/GalaxyCSharp.dll 78 | 79 | 80 | $(HollowKnightRefs)/Mods/ItemChanger/ItemChanger.dll 81 | 82 | 83 | $(HollowKnightRefs)/Mods/MenuChanger/MenuChanger.dll 84 | 85 | 86 | $(HollowKnightRefs)/MMHOOK_Assembly-CSharp.dll 87 | 88 | 89 | $(HollowKnightRefs)/MMHOOK_PlayMaker.dll 90 | 91 | 92 | $(HollowKnightRefs)/Mono.Cecil.dll 93 | 94 | 95 | $(HollowKnightRefs)/Mono.Security.dll 96 | 97 | 98 | $(HollowKnightRefs)/MonoMod.RuntimeDetour.dll 99 | 100 | 101 | $(HollowKnightRefs)/MonoMod.Utils.dll 102 | 103 | 104 | $(HollowKnightRefs)/netstandard.dll 105 | 106 | 107 | $(HollowKnightRefs)/PlayMaker.dll 108 | 109 | 110 | $(HollowKnightRefs)/System.ComponentModel.Composition.dll 111 | 112 | 113 | $(HollowKnightRefs)/System.Configuration.dll 114 | 115 | 116 | $(HollowKnightRefs)/System.Diagnostics.StackTrace.dll 117 | 118 | 119 | $(HollowKnightRefs)/System.EnterpriseServices.dll 120 | 121 | 122 | $(HollowKnightRefs)/System.Globalization.Extensions.dll 123 | 124 | 125 | 126 | 127 | 128 | $(HollowKnightRefs)/System.Transactions.dll 129 | 130 | 131 | $(HollowKnightRefs)/System.Xml.XPath.XDocument.dll 132 | 133 | 134 | $(HollowKnightRefs)/Unity.Timeline.dll 135 | 136 | 137 | $(HollowKnightRefs)/UnityEngine.dll 138 | 139 | 140 | $(HollowKnightRefs)/UnityEngine.AccessibilityModule.dll 141 | 142 | 143 | $(HollowKnightRefs)/UnityEngine.AIModule.dll 144 | 145 | 146 | $(HollowKnightRefs)/UnityEngine.AndroidJNIModule.dll 147 | 148 | 149 | $(HollowKnightRefs)/UnityEngine.AnimationModule.dll 150 | 151 | 152 | $(HollowKnightRefs)/UnityEngine.ARModule.dll 153 | 154 | 155 | $(HollowKnightRefs)/UnityEngine.AssetBundleModule.dll 156 | 157 | 158 | $(HollowKnightRefs)/UnityEngine.AudioModule.dll 159 | 160 | 161 | $(HollowKnightRefs)/UnityEngine.ClothModule.dll 162 | 163 | 164 | $(HollowKnightRefs)/UnityEngine.ClusterInputModule.dll 165 | 166 | 167 | $(HollowKnightRefs)/UnityEngine.ClusterRendererModule.dll 168 | 169 | 170 | $(HollowKnightRefs)/UnityEngine.CoreModule.dll 171 | 172 | 173 | $(HollowKnightRefs)/UnityEngine.CrashReportingModule.dll 174 | 175 | 176 | $(HollowKnightRefs)/UnityEngine.DirectorModule.dll 177 | 178 | 179 | $(HollowKnightRefs)/UnityEngine.DSPGraphModule.dll 180 | 181 | 182 | $(HollowKnightRefs)/UnityEngine.GameCenterModule.dll 183 | 184 | 185 | $(HollowKnightRefs)/UnityEngine.GIModule.dll 186 | 187 | 188 | $(HollowKnightRefs)/UnityEngine.GridModule.dll 189 | 190 | 191 | $(HollowKnightRefs)/UnityEngine.HotReloadModule.dll 192 | 193 | 194 | $(HollowKnightRefs)/UnityEngine.ImageConversionModule.dll 195 | 196 | 197 | $(HollowKnightRefs)/UnityEngine.IMGUIModule.dll 198 | 199 | 200 | $(HollowKnightRefs)/UnityEngine.InputLegacyModule.dll 201 | 202 | 203 | $(HollowKnightRefs)/UnityEngine.InputModule.dll 204 | 205 | 206 | $(HollowKnightRefs)/UnityEngine.JSONSerializeModule.dll 207 | 208 | 209 | $(HollowKnightRefs)/UnityEngine.LocalizationModule.dll 210 | 211 | 212 | $(HollowKnightRefs)/UnityEngine.ParticleSystemModule.dll 213 | 214 | 215 | $(HollowKnightRefs)/UnityEngine.PerformanceReportingModule.dll 216 | 217 | 218 | $(HollowKnightRefs)/UnityEngine.Physics2DModule.dll 219 | 220 | 221 | $(HollowKnightRefs)/UnityEngine.PhysicsModule.dll 222 | 223 | 224 | $(HollowKnightRefs)/UnityEngine.ProfilerModule.dll 225 | 226 | 227 | $(HollowKnightRefs)/UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule.dll 228 | 229 | 230 | $(HollowKnightRefs)/UnityEngine.ScreenCaptureModule.dll 231 | 232 | 233 | $(HollowKnightRefs)/UnityEngine.SharedInternalsModule.dll 234 | 235 | 236 | $(HollowKnightRefs)/UnityEngine.SpriteMaskModule.dll 237 | 238 | 239 | $(HollowKnightRefs)/UnityEngine.SpriteShapeModule.dll 240 | 241 | 242 | $(HollowKnightRefs)/UnityEngine.StreamingModule.dll 243 | 244 | 245 | $(HollowKnightRefs)/UnityEngine.SubstanceModule.dll 246 | 247 | 248 | $(HollowKnightRefs)/UnityEngine.SubsystemsModule.dll 249 | 250 | 251 | $(HollowKnightRefs)/UnityEngine.TerrainModule.dll 252 | 253 | 254 | $(HollowKnightRefs)/UnityEngine.TerrainPhysicsModule.dll 255 | 256 | 257 | $(HollowKnightRefs)/UnityEngine.TextCoreModule.dll 258 | 259 | 260 | $(HollowKnightRefs)/UnityEngine.TextRenderingModule.dll 261 | 262 | 263 | $(HollowKnightRefs)/UnityEngine.TilemapModule.dll 264 | 265 | 266 | $(HollowKnightRefs)/UnityEngine.TLSModule.dll 267 | 268 | 269 | $(HollowKnightRefs)/UnityEngine.UI.dll 270 | 271 | 272 | $(HollowKnightRefs)/UnityEngine.UIElementsModule.dll 273 | 274 | 275 | $(HollowKnightRefs)/UnityEngine.UIElementsNativeModule.dll 276 | 277 | 278 | $(HollowKnightRefs)/UnityEngine.UIModule.dll 279 | 280 | 281 | $(HollowKnightRefs)/UnityEngine.UmbraModule.dll 282 | 283 | 284 | $(HollowKnightRefs)/UnityEngine.UNETModule.dll 285 | 286 | 287 | $(HollowKnightRefs)/UnityEngine.UnityAnalyticsModule.dll 288 | 289 | 290 | $(HollowKnightRefs)/UnityEngine.UnityConnectModule.dll 291 | 292 | 293 | $(HollowKnightRefs)/UnityEngine.UnityCurlModule.dll 294 | 295 | 296 | $(HollowKnightRefs)/UnityEngine.UnityTestProtocolModule.dll 297 | 298 | 299 | $(HollowKnightRefs)/UnityEngine.UnityWebRequestAssetBundleModule.dll 300 | 301 | 302 | $(HollowKnightRefs)/UnityEngine.UnityWebRequestAudioModule.dll 303 | 304 | 305 | $(HollowKnightRefs)/UnityEngine.UnityWebRequestModule.dll 306 | 307 | 308 | $(HollowKnightRefs)/UnityEngine.UnityWebRequestTextureModule.dll 309 | 310 | 311 | $(HollowKnightRefs)/UnityEngine.UnityWebRequestWWWModule.dll 312 | 313 | 314 | $(HollowKnightRefs)/UnityEngine.VehiclesModule.dll 315 | 316 | 317 | $(HollowKnightRefs)/UnityEngine.VFXModule.dll 318 | 319 | 320 | $(HollowKnightRefs)/UnityEngine.VideoModule.dll 321 | 322 | 323 | $(HollowKnightRefs)/UnityEngine.VirtualTexturingModule.dll 324 | 325 | 326 | $(HollowKnightRefs)/UnityEngine.VRModule.dll 327 | 328 | 329 | $(HollowKnightRefs)/UnityEngine.WindModule.dll 330 | 331 | 332 | $(HollowKnightRefs)/UnityEngine.XRModule.dll 333 | 334 | 335 | --------------------------------------------------------------------------------