added Mod files
35
Mod/Archipelago.HollowKnight.sln
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32228.430
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Archipelago.HollowKnight", "Archipelago.HollowKnight\Archipelago.HollowKnight.csproj", "{7B597421-1AA1-4880-B095-7A293B7FF39E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EA5CCD5F-7520-420B-B06F-5506BB4E18F6}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.gitattributes = .gitattributes
|
||||
.gitignore = .gitignore
|
||||
.github\workflows\auto-dependabot.yml = .github\workflows\auto-dependabot.yml
|
||||
.github\workflows\build-release.yml = .github\workflows\build-release.yml
|
||||
.github\dependabot.yml = .github\dependabot.yml
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{7B597421-1AA1-4880-B095-7A293B7FF39E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7B597421-1AA1-4880-B095-7A293B7FF39E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7B597421-1AA1-4880-B095-7A293B7FF39E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7B597421-1AA1-4880-B095-7A293B7FF39E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {3054058D-AA08-4F84-AFE6-6665F9B3BBA2}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
335
Mod/Archipelago.HollowKnight/Archipelago.HollowKnight.csproj
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Archipelago.HollowKnight</RootNamespace>
|
||||
<AssemblyName>Archipelago.HollowKnight</AssemblyName>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<AssemblyTitle>HollowKnight.Archipelago</AssemblyTitle>
|
||||
<Product>HollowKnight.Archipelago</Product>
|
||||
<Description>The Archipelago Multiworld client for Hollow Knight</Description>
|
||||
<Copyright>
|
||||
</Copyright>
|
||||
<Version>0.11.0</Version>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Company>The Archipelago Community</Company>
|
||||
<RepositoryUrl>https://github.com/ArchipelagoMW-HollowKnight/Archipelago.HollowKnight</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<ModName>Archipelago</ModName>
|
||||
<HollowKnightRefs>../API</HollowKnightRefs>
|
||||
<ExportDir>bin/Publish</ExportDir>
|
||||
<Authors>Hussein Farran, Daniel Grace, BadMagic, KonoTyran, Dragonglove</Authors>
|
||||
<AdditionalFileItemNames>EmbeddedResource</AdditionalFileItemNames>
|
||||
</PropertyGroup>
|
||||
<!--
|
||||
Create this file somewhere in the project directory. It should contain a property group with HollowKnightRefs pointing to your Managed folder.
|
||||
If you use Visual Studio, the HK Modding extension has a template for this file which will autodetect your installation.
|
||||
-->
|
||||
<Import Project="LocalOverrides.targets" Condition="Exists('LocalOverrides.targets')" />
|
||||
<Target Name="CopyMod" AfterTargets="PostBuildEvent">
|
||||
<RemoveDir Directories="$(ExportDir)/" />
|
||||
<MakeDir Directories="$(ExportDir)/" />
|
||||
<MakeDir Directories="$(ExportDir)/zip/" />
|
||||
<MakeDir Condition="!Exists('$(HollowKnightRefs)/Mods/$(ModName)/')" Directories="$(HollowKnightRefs)/Mods/$(ModName)/" />
|
||||
<Copy SourceFiles="$(TargetPath);$(TargetDir)/$(TargetName).pdb" DestinationFolder="$(HollowKnightRefs)/Mods/$(ModName)/" />
|
||||
<Copy SourceFiles="$(OutputPath)/Archipelago.MultiClient.Net.dll" DestinationFolder="$(HollowKnightRefs)/Mods/$(ModName)/" />
|
||||
<Copy SourceFiles="$(OutputPath)/Archipelago.Gifting.Net.dll" DestinationFolder="$(HollowKnightRefs)/Mods/$(ModName)/" />
|
||||
<Copy SourceFiles="../README.md;$(TargetPath);$(TargetDir)/$(TargetName).pdb" DestinationFolder="$(ExportDir)/zip/" />
|
||||
<Copy SourceFiles="$(OutputPath)/Archipelago.MultiClient.Net.dll" DestinationFolder="$(ExportDir)/zip/" />
|
||||
<Copy SourceFiles="$(OutputPath)/Archipelago.Gifting.Net.dll" DestinationFolder="$(ExportDir)/zip/" />
|
||||
<ZipDirectory SourceDirectory="$(ExportDir)/zip/" DestinationFile="$(ExportDir)/$(ModName).zip" />
|
||||
<RemoveDir Directories="$(ExportDir)/zip/" />
|
||||
<GetFileHash Files="$(ExportDir)/$(ModName).zip" Algorithm="SHA256">
|
||||
<Output TaskParameter="Items" ItemName="FilesWithHashes" />
|
||||
</GetFileHash>
|
||||
<WriteLinesToFile File="$(ExportDir)/SHA.txt" Lines="@(FilesWithHashes->'%(FileHash)')" Overwrite="true" Encoding="UTF-8" />
|
||||
</Target>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\**\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Archipelago.MultiClient.Net" Version="6.6.1" />
|
||||
<PackageReference Include="Archipelago.MultiClient.Net.Analyzers" Version="1.5.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Archipelago.Gifting.Net" Version="0.4.3" />
|
||||
<PackageReference Include="PolySharp" Version="1.15.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="RandoConstantGenerators" Version="1.2.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Assembly-CSharp">
|
||||
<HintPath>$(HollowKnightRefs)/Assembly-CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Assembly-CSharp-firstpass">
|
||||
<HintPath>$(HollowKnightRefs)/Assembly-CSharp-firstpass.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Benchwarp">
|
||||
<HintPath>$(HollowKnightRefs)/Mods/Benchwarp/Benchwarp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="GalaxyCSharp">
|
||||
<HintPath>$(HollowKnightRefs)/GalaxyCSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ItemChanger">
|
||||
<HintPath>$(HollowKnightRefs)/Mods/ItemChanger/ItemChanger.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MenuChanger">
|
||||
<HintPath>$(HollowKnightRefs)/Mods/MenuChanger/MenuChanger.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MMHOOK_Assembly-CSharp">
|
||||
<HintPath>$(HollowKnightRefs)/MMHOOK_Assembly-CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MMHOOK_PlayMaker">
|
||||
<HintPath>$(HollowKnightRefs)/MMHOOK_PlayMaker.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Mono.Cecil">
|
||||
<HintPath>$(HollowKnightRefs)/Mono.Cecil.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Mono.Security">
|
||||
<HintPath>$(HollowKnightRefs)/Mono.Security.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MonoMod.RuntimeDetour">
|
||||
<HintPath>$(HollowKnightRefs)/MonoMod.RuntimeDetour.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MonoMod.Utils">
|
||||
<HintPath>$(HollowKnightRefs)/MonoMod.Utils.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="netstandard">
|
||||
<HintPath>$(HollowKnightRefs)/netstandard.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PlayMaker">
|
||||
<HintPath>$(HollowKnightRefs)/PlayMaker.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.ComponentModel.Composition">
|
||||
<HintPath>$(HollowKnightRefs)/System.ComponentModel.Composition.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Configuration">
|
||||
<HintPath>$(HollowKnightRefs)/System.Configuration.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Diagnostics.StackTrace">
|
||||
<HintPath>$(HollowKnightRefs)/System.Diagnostics.StackTrace.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.EnterpriseServices">
|
||||
<HintPath>$(HollowKnightRefs)/System.EnterpriseServices.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Globalization.Extensions">
|
||||
<HintPath>$(HollowKnightRefs)/System.Globalization.Extensions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Runtime.Serialization.Xml" />
|
||||
<Reference Include="System.Transactions">
|
||||
<HintPath>$(HollowKnightRefs)/System.Transactions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Xml.XPath.XDocument">
|
||||
<HintPath>$(HollowKnightRefs)/System.Xml.XPath.XDocument.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Unity.Timeline">
|
||||
<HintPath>$(HollowKnightRefs)/Unity.Timeline.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AccessibilityModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.AccessibilityModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AIModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.AIModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AndroidJNIModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.AndroidJNIModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AnimationModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.AnimationModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ARModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.ARModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AssetBundleModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.AssetBundleModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AudioModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.AudioModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ClothModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.ClothModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ClusterInputModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.ClusterInputModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ClusterRendererModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.ClusterRendererModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.CoreModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.CoreModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.CrashReportingModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.CrashReportingModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.DirectorModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.DirectorModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.DSPGraphModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.DSPGraphModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.GameCenterModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.GameCenterModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.GIModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.GIModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.GridModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.GridModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.HotReloadModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.HotReloadModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ImageConversionModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.ImageConversionModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.IMGUIModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.IMGUIModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.InputLegacyModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.InputLegacyModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.InputModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.InputModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.JSONSerializeModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.JSONSerializeModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.LocalizationModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.LocalizationModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ParticleSystemModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.ParticleSystemModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.PerformanceReportingModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.PerformanceReportingModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.Physics2DModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.Physics2DModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.PhysicsModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.PhysicsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ProfilerModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.ProfilerModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ScreenCaptureModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.ScreenCaptureModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.SharedInternalsModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.SharedInternalsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.SpriteMaskModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.SpriteMaskModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.SpriteShapeModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.SpriteShapeModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.StreamingModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.StreamingModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.SubstanceModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.SubstanceModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.SubsystemsModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.SubsystemsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TerrainModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.TerrainModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TerrainPhysicsModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.TerrainPhysicsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TextCoreModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.TextCoreModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TextRenderingModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.TextRenderingModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TilemapModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.TilemapModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TLSModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.TLSModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UI">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UI.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UIElementsModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UIElementsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UIElementsNativeModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UIElementsNativeModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UIModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UIModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UmbraModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UmbraModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UNETModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UNETModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityAnalyticsModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UnityAnalyticsModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityConnectModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UnityConnectModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityCurlModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UnityCurlModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityTestProtocolModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UnityTestProtocolModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityWebRequestAssetBundleModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UnityWebRequestAssetBundleModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityWebRequestAudioModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UnityWebRequestAudioModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityWebRequestModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UnityWebRequestModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityWebRequestTextureModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UnityWebRequestTextureModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UnityWebRequestWWWModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.UnityWebRequestWWWModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.VehiclesModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.VehiclesModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.VFXModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.VFXModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.VideoModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.VideoModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.VirtualTexturingModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.VirtualTexturingModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.VRModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.VRModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.WindModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.WindModule.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.XRModule">
|
||||
<HintPath>$(HollowKnightRefs)/UnityEngine.XRModule.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
273
Mod/Archipelago.HollowKnight/ArchipelagoMod.cs
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
using Archipelago.HollowKnight.IC.Modules;
|
||||
using Archipelago.HollowKnight.MC;
|
||||
using Archipelago.HollowKnight.SlotDataModel;
|
||||
using Archipelago.MultiClient.Net;
|
||||
using Archipelago.MultiClient.Net.Enums;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Internal;
|
||||
using Modding;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Archipelago.HollowKnight
|
||||
{
|
||||
public class ArchipelagoMod : Mod, IGlobalSettings<APGlobalSettings>, ILocalSettings<APLocalSettings>, IMenuMod
|
||||
{
|
||||
// Events support
|
||||
public static event Action OnArchipelagoGameStarted;
|
||||
public static event Action OnArchipelagoGameEnded;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum Archipelago Protocol Version
|
||||
/// </summary>
|
||||
private readonly Version ArchipelagoProtocolVersion = new(0, 5, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Mod version as reported to the modding API
|
||||
/// </summary>
|
||||
public override string GetVersion()
|
||||
{
|
||||
Version assemblyVersion = GetType().Assembly.GetName().Version;
|
||||
string version = $"{assemblyVersion.Major}.{assemblyVersion.Minor}.{assemblyVersion.Build}";
|
||||
#if DEBUG
|
||||
using SHA1 sha = SHA1.Create();
|
||||
using FileStream str = File.OpenRead(GetType().Assembly.Location);
|
||||
StringBuilder sb = new();
|
||||
foreach (byte b in sha.ComputeHash(str).Take(4))
|
||||
{
|
||||
sb.AppendFormat("{0:x2}", b);
|
||||
}
|
||||
version += "-prerelease+" + sb.ToString();
|
||||
#endif
|
||||
return version;
|
||||
}
|
||||
public static ArchipelagoMod Instance;
|
||||
public ArchipelagoSession session { get; private set; }
|
||||
public SlotData SlotData { get; private set; }
|
||||
public bool ArchipelagoEnabled { get; set; }
|
||||
|
||||
public bool ToggleButtonInsideMenu => false;
|
||||
|
||||
internal SpriteManager spriteManager;
|
||||
|
||||
internal APGlobalSettings GS = new();
|
||||
internal APLocalSettings LS = new();
|
||||
|
||||
public ArchipelagoMod() : base("Archipelago") { }
|
||||
|
||||
public override void Initialize(Dictionary<string, Dictionary<string, GameObject>> preloadedObjects)
|
||||
{
|
||||
base.Initialize();
|
||||
Log("Initializing");
|
||||
Instance = this;
|
||||
spriteManager = new SpriteManager(typeof(ArchipelagoMod).Assembly, "Archipelago.HollowKnight.Resources.");
|
||||
|
||||
MenuChanger.ModeMenu.AddMode(new ArchipelagoModeMenuConstructor());
|
||||
Log("Initialized");
|
||||
}
|
||||
|
||||
public void EndGame()
|
||||
{
|
||||
LogDebug("Ending Archipelago game");
|
||||
try
|
||||
{
|
||||
OnArchipelagoGameEnded?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"Error invoking OnArchipelagoGameEnded:\n {ex}");
|
||||
}
|
||||
|
||||
DisconnectArchipelago();
|
||||
ArchipelagoEnabled = false;
|
||||
LS = new();
|
||||
|
||||
Events.OnItemChangerUnhook -= EndGame;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call when starting or resuming a game to randomize and restore state.
|
||||
/// </summary>
|
||||
public void StartOrResumeGame(bool randomize)
|
||||
{
|
||||
if (!ArchipelagoEnabled)
|
||||
{
|
||||
LogDebug("StartOrResumeGame: This is not an Archipelago Game, so not doing anything.");
|
||||
return;
|
||||
}
|
||||
|
||||
LogDebug("StartOrResumeGame: This is an Archipelago Game.");
|
||||
|
||||
LoginSuccessful loginResult = ConnectToArchipelago();
|
||||
|
||||
if (randomize)
|
||||
{
|
||||
LogDebug("StartOrResumeGame: Beginning first time randomization.");
|
||||
LS.Seed = SlotData.Seed;
|
||||
LS.RoomSeed = session.RoomState.Seed;
|
||||
|
||||
LogDebug($"StartOrResumeGame: Room: {LS.RoomSeed}; Seed = {LS.RoomSeed}");
|
||||
|
||||
ArchipelagoRandomizer randomizer = new(SlotData);
|
||||
randomizer.Randomize();
|
||||
}
|
||||
else
|
||||
{
|
||||
LogDebug($"StartOrResumeGame: Local : Room: {LS.RoomSeed}; Seed = {LS.Seed}");
|
||||
int seed = SlotData.Seed;
|
||||
LogDebug($"StartOrResumeGame: AP : Room: {session.RoomState.Seed}; Seed = {seed}");
|
||||
if (seed != LS.Seed || session.RoomState.Seed != LS.RoomSeed)
|
||||
{
|
||||
throw new LoginValidationException("Slot mismatch. Saved seed does not match the server value. Is this the correct save?");
|
||||
}
|
||||
}
|
||||
|
||||
// check the goal is one we know how to cope with
|
||||
if (SlotData.Options.Goal > GoalsLookup.MAX)
|
||||
{
|
||||
throw new LoginValidationException($"Unrecognized goal condition {SlotData.Options.Goal} (are you running an outdated client?)");
|
||||
}
|
||||
|
||||
// Hooks happen after we've definitively connected to an Archipelago slot correctly.
|
||||
// Doing this before checking for the correct slot/seed/room will cause problems if
|
||||
// the client connects to the wrong session with a matching slot.
|
||||
Events.OnItemChangerUnhook += EndGame;
|
||||
|
||||
try
|
||||
{
|
||||
OnArchipelagoGameStarted?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"Error invoking OnArchipelagoGameStarted:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSocketClosed(string reason)
|
||||
{
|
||||
ItemChangerMod.Modules.Get<ItemNetworkingModule>().ReportDisconnect();
|
||||
}
|
||||
|
||||
private LoginSuccessful ConnectToArchipelago()
|
||||
{
|
||||
session = ArchipelagoSessionFactory.CreateSession(LS.ConnectionDetails.ServerUrl, LS.ConnectionDetails.ServerPort);
|
||||
|
||||
LoginResult loginResult = session.TryConnectAndLogin("Hollow Knight Test",
|
||||
LS.ConnectionDetails.SlotName,
|
||||
ItemsHandlingFlags.AllItems,
|
||||
ArchipelagoProtocolVersion,
|
||||
password: LS.ConnectionDetails.ServerPassword,
|
||||
requestSlotData: false);
|
||||
|
||||
if (loginResult is LoginFailure failure)
|
||||
{
|
||||
string errors = string.Join(", ", failure.Errors);
|
||||
LogError($"Unable to connect to Archipelago because: {string.Join(", ", failure.Errors)}");
|
||||
throw new LoginValidationException(errors);
|
||||
}
|
||||
else if (loginResult is LoginSuccessful success)
|
||||
{
|
||||
SlotData = session.DataStorage.GetSlotData<SlotData>();
|
||||
session.Socket.SocketClosed += OnSocketClosed;
|
||||
|
||||
return success;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogError($"Unexpected LoginResult type when connecting to Archipelago: {loginResult}");
|
||||
throw new LoginValidationException("Unexpected login result.");
|
||||
}
|
||||
}
|
||||
|
||||
public void DisconnectArchipelago()
|
||||
{
|
||||
if (session?.Socket != null)
|
||||
{
|
||||
session.Socket.SocketClosed -= OnSocketClosed;
|
||||
}
|
||||
|
||||
if (session?.Socket != null && session.Socket.Connected)
|
||||
{
|
||||
session.Socket.DisconnectAsync();
|
||||
}
|
||||
|
||||
session = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when loading local (game-specific save data)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="details"></param>
|
||||
public void OnLoadLocal(APLocalSettings ls)
|
||||
{
|
||||
if (ls.ConnectionDetails == null
|
||||
|| ls.ConnectionDetails.SlotName == null
|
||||
|| ls.ConnectionDetails.SlotName == "") // Apparently, this is called even before a save is loaded. Catch this.
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LS = ls;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when saving local (game-specific) save data.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public APLocalSettings OnSaveLocal()
|
||||
{
|
||||
if (!ArchipelagoEnabled)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return LS;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when loading global save data.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="details"></param>
|
||||
public void OnLoadGlobal(APGlobalSettings gs)
|
||||
{
|
||||
GS = gs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when saving global save data.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public APGlobalSettings OnSaveGlobal()
|
||||
{
|
||||
APGlobalSettings r = GS with
|
||||
{
|
||||
MenuConnectionDetails = GS.MenuConnectionDetails with { ServerPassword = null }
|
||||
};
|
||||
return r;
|
||||
}
|
||||
|
||||
public List<IMenuMod.MenuEntry> GetMenuData(IMenuMod.MenuEntry? toggleButtonEntry)
|
||||
{
|
||||
return [
|
||||
new IMenuMod.MenuEntry(
|
||||
"Enable Gifting",
|
||||
["false", "true"],
|
||||
"Enable or disable interaction with the Archipelago gifting system. Requires reloading the save to take effect.",
|
||||
(v) => GS.EnableGifting = v == 1,
|
||||
() => GS.EnableGifting ? 1 : 0
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
397
Mod/Archipelago.HollowKnight/ArchipelagoRandomizer.cs
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
using Archipelago.HollowKnight.IC;
|
||||
using Archipelago.HollowKnight.IC.Modules;
|
||||
using Archipelago.HollowKnight.IC.RM;
|
||||
using Archipelago.HollowKnight.SlotDataModel;
|
||||
using Archipelago.MultiClient.Net;
|
||||
using Archipelago.MultiClient.Net.Models;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Extensions;
|
||||
using ItemChanger.Modules;
|
||||
using ItemChanger.Placements;
|
||||
using ItemChanger.Tags;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Archipelago.HollowKnight
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks state only required during initial randomization
|
||||
/// </summary>
|
||||
internal class ArchipelagoRandomizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Randomized charm notch costs as stored in slot data.
|
||||
/// </summary>
|
||||
public List<int> NotchCosts { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracks created placements and their associated locations during randomization.
|
||||
/// </summary>
|
||||
public Dictionary<string, AbstractPlacement> placements = new();
|
||||
|
||||
/// <summary>
|
||||
/// Seeded RNG for clientside randomization.
|
||||
/// </summary>
|
||||
public readonly Random Random;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for IC item creation
|
||||
/// </summary>
|
||||
public readonly ItemFactory itemFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for IC cost creation
|
||||
/// </summary>
|
||||
public readonly CostFactory costFactory;
|
||||
|
||||
private readonly SlotData SlotData;
|
||||
private ArchipelagoSession Session => ArchipelagoMod.Instance.session;
|
||||
private ArchipelagoMod Instance => ArchipelagoMod.Instance;
|
||||
|
||||
public ArchipelagoRandomizer(SlotData slotData)
|
||||
{
|
||||
SlotData = slotData;
|
||||
Random = new Random(slotData.Seed);
|
||||
itemFactory = new ItemFactory();
|
||||
costFactory = new CostFactory(slotData.LocationCosts);
|
||||
NotchCosts = slotData.NotchCosts;
|
||||
|
||||
ArchipelagoMod.Instance.Log("Initializing ArchipelagoRandomizer with slot data: " + JsonConvert.SerializeObject(SlotData));
|
||||
}
|
||||
|
||||
public void Randomize()
|
||||
{
|
||||
ArchipelagoSession session = Session;
|
||||
ItemChangerMod.CreateSettingsProfile();
|
||||
if (SlotData.Options.StartLocationName is string start)
|
||||
{
|
||||
if (IC.RM.StartDef.Lookup.TryGetValue(start, out IC.RM.StartDef def))
|
||||
{
|
||||
ItemChangerMod.ChangeStartGame(def.ToItemChangerStartDef());
|
||||
ArchipelagoMod.Instance.Log($"Set start to {start}");
|
||||
}
|
||||
else
|
||||
{
|
||||
ArchipelagoMod.Instance.LogError($"Unsupported start location {start}, starting in King's Pass");
|
||||
}
|
||||
}
|
||||
|
||||
// Add IC modules as needed
|
||||
// FUTURE: If Entrance rando, disable palace midwarp and some logical blockers
|
||||
// if (Entrance Rando Is Enabled) {
|
||||
// ItemChangerMod.Modules.Add<ItemChanger.Modules.DisablePalaceMidWarp>();
|
||||
// ItemChangerMod.Modules.Add<ItemChanger.Modules.RemoveInfectedBlockades>();
|
||||
// }
|
||||
|
||||
AddItemChangerModules();
|
||||
AddHelperPlatforms();
|
||||
|
||||
ApplyCharmCosts();
|
||||
|
||||
// Initialize shop locations in case they end up with zero items placed.
|
||||
AbstractLocation location;
|
||||
AbstractPlacement pmt;
|
||||
|
||||
string[] shops = [
|
||||
LocationNames.Sly, LocationNames.Sly_Key, LocationNames.Iselda,
|
||||
LocationNames.Salubra, LocationNames.Leg_Eater, LocationNames.Grubfather,
|
||||
LocationNames.Seer
|
||||
];
|
||||
foreach (string name in shops)
|
||||
{
|
||||
location = Finder.GetLocation(name);
|
||||
placements[name] = pmt = location.Wrap();
|
||||
|
||||
pmt.AddTag<ArchipelagoPlacementTag>();
|
||||
|
||||
if (pmt is ShopPlacement shop)
|
||||
{
|
||||
shop.defaultShopItems = DefaultShopItems.IseldaMapPins
|
||||
| DefaultShopItems.IseldaMapMarkers
|
||||
| DefaultShopItems.LegEaterRepair;
|
||||
if (SlotData.Options.AddUnshuffledLocations)
|
||||
{
|
||||
// AP will add the default items on our behalf
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!SlotData.Options.RandomizeCharms)
|
||||
{
|
||||
shop.defaultShopItems |= DefaultShopItems.SlyCharms
|
||||
| DefaultShopItems.SlyKeyCharms
|
||||
| DefaultShopItems.IseldaCharms
|
||||
| DefaultShopItems.SalubraCharms
|
||||
| DefaultShopItems.LegEaterCharms;
|
||||
}
|
||||
if (!SlotData.Options.RandomizeMaps)
|
||||
{
|
||||
shop.defaultShopItems |= DefaultShopItems.IseldaMaps
|
||||
| DefaultShopItems.IseldaQuill;
|
||||
}
|
||||
if (!SlotData.Options.RandomizeCharmNotches)
|
||||
{
|
||||
shop.defaultShopItems |= DefaultShopItems.SalubraNotches
|
||||
| DefaultShopItems.SalubraBlessing;
|
||||
}
|
||||
if (!SlotData.Options.RandomizeKeys)
|
||||
{
|
||||
shop.defaultShopItems |= DefaultShopItems.SlySimpleKey
|
||||
| DefaultShopItems.SlyLantern
|
||||
| DefaultShopItems.SlyKeyElegantKey;
|
||||
}
|
||||
if (!SlotData.Options.RandomizeMaskShards)
|
||||
{
|
||||
shop.defaultShopItems |= DefaultShopItems.SlyMaskShards;
|
||||
}
|
||||
if (!SlotData.Options.RandomizeVesselFragments)
|
||||
{
|
||||
shop.defaultShopItems |= DefaultShopItems.SlyVesselFragments;
|
||||
}
|
||||
if (!SlotData.Options.RandomizeRancidEggs)
|
||||
{
|
||||
shop.defaultShopItems |= DefaultShopItems.SlyRancidEgg;
|
||||
}
|
||||
}
|
||||
else if (name == LocationNames.Grubfather)
|
||||
{
|
||||
DestroyGrubRewardTag t = pmt.AddTag<DestroyGrubRewardTag>();
|
||||
t.destroyRewards = GrubfatherRewards.None;
|
||||
if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeMaskShards)
|
||||
{
|
||||
t.destroyRewards |= GrubfatherRewards.MaskShard;
|
||||
}
|
||||
if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeCharms)
|
||||
{
|
||||
t.destroyRewards |= GrubfatherRewards.Grubsong | GrubfatherRewards.GrubberflysElegy;
|
||||
}
|
||||
if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeRancidEggs)
|
||||
{
|
||||
t.destroyRewards |= GrubfatherRewards.RancidEgg;
|
||||
}
|
||||
if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeRelics)
|
||||
{
|
||||
t.destroyRewards |= GrubfatherRewards.HallownestSeal | GrubfatherRewards.KingsIdol;
|
||||
}
|
||||
if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizePaleOre)
|
||||
{
|
||||
t.destroyRewards |= GrubfatherRewards.PaleOre;
|
||||
}
|
||||
}
|
||||
else if (name == LocationNames.Seer)
|
||||
{
|
||||
DestroySeerRewardTag t = pmt.AddTag<DestroySeerRewardTag>();
|
||||
t.destroyRewards = SeerRewards.None;
|
||||
if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeRelics)
|
||||
{
|
||||
t.destroyRewards |= SeerRewards.HallownestSeal | SeerRewards.ArcaneEgg;
|
||||
}
|
||||
if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizePaleOre)
|
||||
{
|
||||
t.destroyRewards |= SeerRewards.PaleOre;
|
||||
}
|
||||
if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeCharms)
|
||||
{
|
||||
t.destroyRewards |= SeerRewards.DreamWielder;
|
||||
}
|
||||
if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeVesselFragments)
|
||||
{
|
||||
t.destroyRewards |= SeerRewards.VesselFragment;
|
||||
}
|
||||
if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeSkills)
|
||||
{
|
||||
t.destroyRewards |= SeerRewards.DreamGate | SeerRewards.AwokenDreamNail;
|
||||
}
|
||||
if (SlotData.Options.AddUnshuffledLocations || SlotData.Options.RandomizeMaskShards)
|
||||
{
|
||||
t.destroyRewards |= SeerRewards.MaskShard;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task<Dictionary<long, ScoutedItemInfo>> scoutTask = session.Locations
|
||||
.ScoutLocationsAsync(session.Locations.AllLocations.ToArray());
|
||||
scoutTask.Wait();
|
||||
|
||||
Dictionary<long, ScoutedItemInfo> scoutResult = scoutTask.Result;
|
||||
foreach (KeyValuePair<long, ScoutedItemInfo> scout in scoutResult)
|
||||
{
|
||||
long id = scout.Key;
|
||||
ScoutedItemInfo item = scout.Value;
|
||||
string itemName = item.ItemName ?? $"?Item {item.ItemId}";
|
||||
PlaceItem(item.LocationName, itemName, item);
|
||||
}
|
||||
ItemChangerMod.AddPlacements(placements.Values);
|
||||
|
||||
}
|
||||
|
||||
private void AddItemChangerModules()
|
||||
{
|
||||
ItemChangerMod.Modules.Add<DupeHandlingModule>();
|
||||
ItemChangerMod.Modules.Add<ItemNetworkingModule>();
|
||||
ItemChangerMod.Modules.Add<GiftingModule>();
|
||||
ItemChangerMod.Modules.Add<GoalModule>();
|
||||
ItemChangerMod.Modules.Add<CompletionPercentOverride>();
|
||||
ItemChangerMod.Modules.Add<HintTracker>();
|
||||
ItemChangerMod.Modules.Add<RepositionShadeModule>();
|
||||
ItemChangerMod.Modules.Add<BenchSyncModule>();
|
||||
ItemChangerMod.Modules.Add<StartLocationSceneEditsModule>();
|
||||
|
||||
if (SlotData.Options.DeathLink)
|
||||
{
|
||||
ItemChangerMod.Modules.Add<DeathLinkModule>();
|
||||
}
|
||||
|
||||
if (SlotData.Options.RandomizeElevatorPass)
|
||||
{
|
||||
ItemChangerMod.Modules.Add<ElevatorPass>();
|
||||
}
|
||||
|
||||
if (SlotData.Options.RandomizeFocus)
|
||||
{
|
||||
ItemChangerMod.Modules.Add<FocusSkill>();
|
||||
}
|
||||
|
||||
if (SlotData.Options.RandomizeSwim)
|
||||
{
|
||||
ItemChangerMod.Modules.Add<SwimSkill>();
|
||||
}
|
||||
|
||||
if (SlotData.Options.SplitMothwingCloak)
|
||||
{
|
||||
ItemChangerMod.Modules.Add<SplitCloak>();
|
||||
}
|
||||
|
||||
if (SlotData.Options.SplitMantisClaw)
|
||||
{
|
||||
ItemChangerMod.Modules.Add<SplitClaw>();
|
||||
}
|
||||
|
||||
if (SlotData.Options.SplitCrystalHeart)
|
||||
{
|
||||
ItemChangerMod.Modules.Add<SplitSuperdash>();
|
||||
}
|
||||
|
||||
if (SlotData.Options.Slopeballs)
|
||||
{
|
||||
ItemChangerMod.Modules.Add<ToggleableFireballUpgrade>();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddHelperPlatforms()
|
||||
{
|
||||
HelperPlatformBuilder.AddConveniencePlatforms(SlotData.Options);
|
||||
HelperPlatformBuilder.AddStartLocationRequiredPlatforms(SlotData.Options);
|
||||
}
|
||||
|
||||
private void ApplyCharmCosts()
|
||||
{
|
||||
bool isNotchCostsRandomizedOrPlando = false;
|
||||
for (int i = 0; i < NotchCosts.Count; i++)
|
||||
{
|
||||
if (PlayerData.instance.GetInt($"charmCost_{i + 1}") != NotchCosts[i])
|
||||
{
|
||||
isNotchCostsRandomizedOrPlando = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isNotchCostsRandomizedOrPlando)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ItemChangerMod.Modules.Add<NotchCostUI>();
|
||||
ItemChangerMod.Modules.Add<ZeroCostCharmEquip>();
|
||||
PlayerDataEditModule playerDataEditModule = ItemChangerMod.Modules.GetOrAdd<PlayerDataEditModule>();
|
||||
Instance.LogDebug(playerDataEditModule);
|
||||
for (int i = 0; i < NotchCosts.Count; i++)
|
||||
{
|
||||
playerDataEditModule.AddPDEdit($"charmCost_{i + 1}", NotchCosts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void PlaceItem(string location, string name, ScoutedItemInfo itemInfo)
|
||||
{
|
||||
Instance.LogDebug($"[PlaceItem] Placing item {name} into {location} with ID {itemInfo.ItemId}");
|
||||
|
||||
string originalLocation = string.Copy(location);
|
||||
location = StripShopSuffix(location);
|
||||
// IC does not like placements at these locations if there's also a location at the lore tablet, it renders the lore tablet inoperable.
|
||||
// But we can have multiple placements at the same location, so do this workaround. (Rando4 does something similar per its README)
|
||||
if (SlotData.Options.RandomizeLoreTablets)
|
||||
{
|
||||
switch (location)
|
||||
{
|
||||
case LocationNames.Focus:
|
||||
location = LocationNames.Lore_Tablet_Kings_Pass_Focus;
|
||||
break;
|
||||
case LocationNames.World_Sense:
|
||||
location = LocationNames.Lore_Tablet_World_Sense;
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
}
|
||||
|
||||
AbstractLocation loc = Finder.GetLocation(location);
|
||||
if (loc == null)
|
||||
{
|
||||
Instance.LogDebug($"[PlaceItem] Location was null: Name: {location}.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool isMyItem = itemInfo.IsReceiverRelatedToActivePlayer;
|
||||
string recipientName = null;
|
||||
if (!isMyItem)
|
||||
{
|
||||
recipientName = Session.Players.GetPlayerName(itemInfo.Player);
|
||||
}
|
||||
|
||||
AbstractPlacement pmt = placements.GetOrDefault(location);
|
||||
if (pmt == null)
|
||||
{
|
||||
pmt = loc.Wrap();
|
||||
pmt.AddTag<ArchipelagoPlacementTag>();
|
||||
placements[location] = pmt;
|
||||
}
|
||||
|
||||
AbstractItem item;
|
||||
if (isMyItem)
|
||||
{
|
||||
item = itemFactory.CreateMyItem(name, itemInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
item = itemFactory.CreateRemoteItem(pmt, recipientName, name, itemInfo);
|
||||
}
|
||||
|
||||
pmt.Add(item);
|
||||
costFactory.ApplyCost(pmt, item, originalLocation);
|
||||
}
|
||||
|
||||
private string StripShopSuffix(string location)
|
||||
{
|
||||
if (string.IsNullOrEmpty(location))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string[] names =
|
||||
[
|
||||
LocationNames.Sly_Key, LocationNames.Sly, LocationNames.Iselda, LocationNames.Salubra,
|
||||
LocationNames.Leg_Eater, LocationNames.Egg_Shop, LocationNames.Seer, LocationNames.Grubfather
|
||||
];
|
||||
|
||||
foreach (string name in names)
|
||||
{
|
||||
if (location.StartsWith(name))
|
||||
{
|
||||
return location.Substring(0, name.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
}
|
||||
}
|
||||
127
Mod/Archipelago.HollowKnight/DeathLinkMessages.cs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Archipelago.HollowKnight
|
||||
{
|
||||
public static class DeathLinkMessages
|
||||
{
|
||||
public static readonly List<string> DefaultMessages = new()
|
||||
{
|
||||
"@ died.",
|
||||
"@ has perished.",
|
||||
"@ made poor life choices.",
|
||||
"@ didn't listen to Hornet's advice.",
|
||||
"@ took damage equal to or more than their current HP.",
|
||||
"@ made a fatal mistake.",
|
||||
"@ threw some shade at @.",
|
||||
"@ decided to set up a Shade Skip.", // A System of Vibrant Colors (edited)
|
||||
"Hopefully @ didn't have a fragile charm equipped.", // Koatlus
|
||||
"A true servant gives all for the Kingdom. Let @ relieve you of your life.", // Koatlus
|
||||
"Through @'s sacrifice, you are now dead.", // Koatlus
|
||||
"The truce remains. Our vigil holds. @ must respawn.", // Koatlus
|
||||
"Hopefully @ didn't have a fragile charm equipped.", // Koatlus
|
||||
};
|
||||
|
||||
public static readonly List<string> UnknownMessages = new()
|
||||
{
|
||||
"@ has died in a manner most unusual.",
|
||||
"@ found a way to break the game, and the game broke @ back.",
|
||||
"@ has lost The Game",
|
||||
};
|
||||
|
||||
public static readonly Dictionary<int, List<string>> MessagesByType = new()
|
||||
{
|
||||
{
|
||||
1, // Deaths from enemy damage
|
||||
new List<string>
|
||||
{
|
||||
"@ has discovered that there are bugs in Hallownest.",
|
||||
"@ should have dodged.",
|
||||
"@ should have jumped.",
|
||||
"@ significantly mistimed their parry attempt.",
|
||||
"@ should have considered equipping Dreamshield.",
|
||||
"@ must have never fought that enemy before.",
|
||||
"@ did not make it to phase 2.",
|
||||
"@ dashed in the wrong direction.", // Murphmario
|
||||
"@ tried to talk it out.", // SnowOfAllTrades
|
||||
"@ made masterful use of their vulnerability frames.",
|
||||
}
|
||||
},
|
||||
{
|
||||
2, // Deaths from spikes
|
||||
new List<string>
|
||||
{
|
||||
"@ was in the wrong place.",
|
||||
"@ mistimed their jump.",
|
||||
"@ didn't see the sharp things.",
|
||||
"@ didn't see that saw.",
|
||||
"@ fought the spikes and the spikes won.",
|
||||
"@ sought roses but found only thorns.",
|
||||
"@ was pricked to death.", // A System of Vibrant Colors
|
||||
"@ dashed in the wrong direction.", // Murphmario
|
||||
"@ found their own Path of Pain.", // Fatman
|
||||
"@ has strayed from the White King's roads.", // Koatlus
|
||||
}
|
||||
},
|
||||
{
|
||||
3, // Deaths from acid
|
||||
new List<string>
|
||||
{
|
||||
"@ was in the wrong place.",
|
||||
"@ mistimed their jump.",
|
||||
"@ forgot their floaties.",
|
||||
"What @ thought was H2O was H2SO4.",
|
||||
"@ wishes they could swim.",
|
||||
"@ used the wrong kind of dive.",
|
||||
"@ got into a fight with a pool of liquid and lost.",
|
||||
"@ forgot how to swim", // squidy
|
||||
}
|
||||
},
|
||||
{
|
||||
999, // Deaths in the dream realm
|
||||
new List<string>
|
||||
{
|
||||
"@ dozed off for good.",
|
||||
"@ was caught sleeping on the job.",
|
||||
"@ sought dreams but found only nightmares.",
|
||||
"@ got lost in Limbo.",
|
||||
"Good night, @.",
|
||||
"@ is resting in pieces.",
|
||||
"@ exploded into a thousand pieces of essence.",
|
||||
"Hey, @, you're finally awake.",
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
private static readonly Random random = new(); // This is only messaging, so does not need to be seeded.
|
||||
|
||||
public static string GetDeathMessage(int cause, string player)
|
||||
{
|
||||
// Build candidate death messages.
|
||||
List<string> messages;
|
||||
bool knownCauseOfDeath = MessagesByType.TryGetValue(cause, out messages);
|
||||
|
||||
if (knownCauseOfDeath)
|
||||
{
|
||||
messages = new(messages);
|
||||
messages.AddRange(DefaultMessages);
|
||||
}
|
||||
else
|
||||
{
|
||||
messages = UnknownMessages;
|
||||
}
|
||||
|
||||
// Choose one at random
|
||||
string message = messages[random.Next(0, messages.Count)].Replace("@", player);
|
||||
|
||||
// If it's an unknown death, tag in some debugging info
|
||||
if (!knownCauseOfDeath)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogWarn($"UNKNOWN cause of death {cause}");
|
||||
message += $" (Type: {cause})";
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
};
|
||||
}
|
||||
46
Mod/Archipelago.HollowKnight/DupeUIDef.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using ItemChanger;
|
||||
using ItemChanger.UIDefs;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Archipelago.HollowKnight;
|
||||
|
||||
public class DupeUIDef : MsgUIDef
|
||||
{
|
||||
public static MsgUIDef Of(UIDef inner)
|
||||
{
|
||||
if (inner is MsgUIDef msg)
|
||||
{
|
||||
return new SplitUIDef
|
||||
{
|
||||
preview = new BoxedString(msg.GetPreviewName()),
|
||||
name = new BoxedString($"Nothing ({msg.GetPostviewName()})"),
|
||||
shopDesc = msg.shopDesc?.Clone(),
|
||||
sprite = msg.sprite?.Clone(),
|
||||
};
|
||||
}
|
||||
return new DupeUIDef(inner);
|
||||
}
|
||||
|
||||
public UIDef Inner { get; set; }
|
||||
private DupeUIDef(UIDef inner)
|
||||
{
|
||||
Inner = inner;
|
||||
sprite = new ItemChangerSprite("ShopIcons.LampBug");
|
||||
if (inner is null)
|
||||
{
|
||||
name = new BoxedString("Nothing (Dupe)");
|
||||
shopDesc = new BoxedString("");
|
||||
}
|
||||
else
|
||||
{
|
||||
// with good practice these should never be accessed but better not to break stuff
|
||||
name = new BoxedString($"Nothing ({inner.GetPostviewName()})");
|
||||
shopDesc = new BoxedString(inner.GetShopDesc());
|
||||
}
|
||||
}
|
||||
|
||||
public override Sprite GetSprite() => Inner is not null ? Inner.GetSprite() : base.GetSprite();
|
||||
public override string GetPreviewName() => Inner is not null ? Inner.GetPreviewName() : base.GetPreviewName();
|
||||
public override string GetPostviewName() => Inner is not null ? Inner.GetPostviewName() : base.GetPostviewName();
|
||||
public override string GetShopDesc() => Inner is not null ? Inner.GetShopDesc() : base.GetShopDesc();
|
||||
}
|
||||
24
Mod/Archipelago.HollowKnight/Enums.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
namespace Archipelago.HollowKnight
|
||||
{
|
||||
public enum DeathLinkStatus
|
||||
{
|
||||
None = 0,
|
||||
Pending = 1,
|
||||
Dying = 2
|
||||
}
|
||||
|
||||
public enum DeathLinkShadeHandling
|
||||
{
|
||||
Vanilla = 0,
|
||||
Shadeless = 1,
|
||||
Shade = 2
|
||||
}
|
||||
|
||||
public enum WhitePalaceOption
|
||||
{
|
||||
Exclude = 0,
|
||||
KingFragment = 1,
|
||||
NoPathOfPain = 2,
|
||||
Include = 3
|
||||
}
|
||||
}
|
||||
16
Mod/Archipelago.HollowKnight/Extensions.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using ItemChanger;
|
||||
|
||||
namespace Archipelago.HollowKnight;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static string GetPreviewWithCost(this AbstractItem item)
|
||||
{
|
||||
string text = item.GetPreviewName();
|
||||
if (item.GetTag(out CostTag tag))
|
||||
{
|
||||
text += " - " + tag.Cost.GetCostText();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
6
Mod/Archipelago.HollowKnight/GeneratedConsts.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
using RandoConstantGenerators;
|
||||
|
||||
namespace Archipelago.HollowKnight;
|
||||
|
||||
[GenerateJsonConsts("$.*~", "Data/starts.json")]
|
||||
public static partial class StartLocationNames { }
|
||||
307
Mod/Archipelago.HollowKnight/Goals.cs
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
using Archipelago.HollowKnight.IC;
|
||||
using Archipelago.HollowKnight.IC.Items;
|
||||
using Archipelago.HollowKnight.IC.Modules;
|
||||
using Archipelago.MultiClient.Net.Exceptions;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Extensions;
|
||||
using ItemChanger.Internal;
|
||||
using ItemChanger.Placements;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Archipelago.HollowKnight
|
||||
{
|
||||
public enum GoalsLookup
|
||||
{
|
||||
Any = 0,
|
||||
HollowKnight = 1,
|
||||
SealedSiblings = 2,
|
||||
Radiance = 3,
|
||||
Godhome = 4,
|
||||
GodhomeFlower = 5,
|
||||
GrubHunt = 6,
|
||||
MAX = GrubHunt
|
||||
}
|
||||
|
||||
public abstract class Goal
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public abstract string Description { get; }
|
||||
public virtual bool CanBeSelectedForAnyGoal => true;
|
||||
|
||||
private static readonly Dictionary<GoalsLookup, Goal> Lookup = new()
|
||||
{
|
||||
[GoalsLookup.HollowKnight] = new HollowKnightGoal(),
|
||||
[GoalsLookup.SealedSiblings] = new SealedSiblingsGoal(),
|
||||
[GoalsLookup.Radiance] = new RadianceGoal(),
|
||||
[GoalsLookup.Godhome] = new GodhomeGoal(),
|
||||
[GoalsLookup.GodhomeFlower] = new GodhomeFlowerGoal(),
|
||||
[GoalsLookup.GrubHunt] = new GrubHuntGoal(),
|
||||
};
|
||||
|
||||
static Goal()
|
||||
{
|
||||
Lookup[GoalsLookup.Any] = new AnyGoal(Lookup.Values.ToList());
|
||||
}
|
||||
|
||||
protected void FountainPlaqueTopEdit(ref string s) => s = "Your goal is";
|
||||
protected void FountainPlaqueNameEdit(ref string s) => s = Name;
|
||||
protected void FountainPlaqueDescEdit(ref string s) => s = Description;
|
||||
|
||||
protected abstract bool VictoryCondition();
|
||||
|
||||
public static Goal GetGoal(GoalsLookup key)
|
||||
{
|
||||
Goal value;
|
||||
if (Lookup.TryGetValue(key, out value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
ArchipelagoMod.Instance.LogError($"Listed goal is {key}, which is greater than {GoalsLookup.MAX}. Is this an outdated client?");
|
||||
throw new ArgumentOutOfRangeException($"Unrecognized goal condition {key} (are you running an outdated client?)");
|
||||
}
|
||||
|
||||
public async Task CheckForVictoryAsync()
|
||||
{
|
||||
ArchipelagoMod.Instance.LogDebug($"Checking for victory; goal is {this.Name}; scene " +
|
||||
$"{UnityEngine.SceneManagement.SceneManager.GetActiveScene().name}");
|
||||
if (VictoryCondition())
|
||||
{
|
||||
ArchipelagoMod.Instance.LogDebug($"Victory detected, declaring!");
|
||||
try
|
||||
{
|
||||
await ItemChangerMod.Modules.Get<GoalModule>().DeclareVictoryAsync().TimeoutAfter(1000);
|
||||
}
|
||||
catch (Exception ex) when (ex is TimeoutException or ArchipelagoSocketClosedException)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogError("Failed to send goal to server");
|
||||
ArchipelagoMod.Instance.LogError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Select()
|
||||
{
|
||||
Events.AddLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_TOP"), FountainPlaqueTopEdit);
|
||||
Events.AddLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_MAIN"), FountainPlaqueNameEdit);
|
||||
Events.AddLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_DESC"), FountainPlaqueDescEdit);
|
||||
OnSelected();
|
||||
}
|
||||
|
||||
public void Deselect()
|
||||
{
|
||||
Events.RemoveLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_TOP"), FountainPlaqueTopEdit);
|
||||
Events.RemoveLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_MAIN"), FountainPlaqueNameEdit);
|
||||
Events.RemoveLanguageEdit(new LanguageKey("Prompts", "FOUNTAIN_PLAQUE_DESC"), FountainPlaqueDescEdit);
|
||||
OnDeselected();
|
||||
}
|
||||
|
||||
public abstract void OnSelected();
|
||||
public abstract void OnDeselected();
|
||||
}
|
||||
|
||||
public class AnyGoal : Goal
|
||||
{
|
||||
private IReadOnlyList<Goal> subgoals;
|
||||
|
||||
public override string Name => "Any Goal";
|
||||
|
||||
public override string Description => "Do whichever goal you like. If you're not sure,<br>try defeating the Hollow Knight!";
|
||||
|
||||
public AnyGoal(IReadOnlyList<Goal> subgoals)
|
||||
{
|
||||
this.subgoals = subgoals;
|
||||
}
|
||||
|
||||
public override void OnSelected()
|
||||
{
|
||||
foreach (Goal goal in subgoals.Where(g => g.CanBeSelectedForAnyGoal))
|
||||
{
|
||||
goal.OnSelected();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDeselected()
|
||||
{
|
||||
foreach (Goal goal in subgoals.Where(g => g.CanBeSelectedForAnyGoal))
|
||||
{
|
||||
goal.OnDeselected();
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool VictoryCondition()
|
||||
{
|
||||
// this goal is never completed on its own, it relies on subgoals to check for victory themselves.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A goal which is achieved by completing a given ending (or harder)
|
||||
/// </summary>
|
||||
public abstract class EndingGoal : Goal
|
||||
{
|
||||
private static List<string> VictoryScenes = new()
|
||||
{
|
||||
SceneNames.Cinematic_Ending_A, // THK
|
||||
SceneNames.Cinematic_Ending_B, // Sealed Siblings
|
||||
SceneNames.Cinematic_Ending_C, // Radiance
|
||||
"Cinematic_Ending_D", // Godhome no flower quest
|
||||
SceneNames.Cinematic_Ending_E // Godhome w/ flower quest
|
||||
};
|
||||
|
||||
public abstract string MinimumGoalScene { get; }
|
||||
|
||||
public override void OnSelected()
|
||||
{
|
||||
Events.OnSceneChange += SceneChanged;
|
||||
}
|
||||
|
||||
public override void OnDeselected()
|
||||
{
|
||||
Events.OnSceneChange -= SceneChanged;
|
||||
}
|
||||
|
||||
private async void SceneChanged(UnityEngine.SceneManagement.Scene obj)
|
||||
{
|
||||
await CheckForVictoryAsync();
|
||||
}
|
||||
|
||||
protected override bool VictoryCondition()
|
||||
{
|
||||
string activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
|
||||
if (activeScene.StartsWith("Cinematic_Ending_"))
|
||||
{
|
||||
int minGoalSceneIndex = VictoryScenes.IndexOf(MinimumGoalScene);
|
||||
int sceneIndex = VictoryScenes.IndexOf(activeScene);
|
||||
return sceneIndex >= minGoalSceneIndex;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A goal which is achieved by obtaining the Victory item placed at a given location in the world
|
||||
/// </summary>
|
||||
public abstract class ItemGoal : Goal
|
||||
{
|
||||
// We don't use CheckForVictoryAsync here, rather, the Victory item uses GoalModule.DeclareVictoryAsync
|
||||
// directly when acquired.
|
||||
protected override bool VictoryCondition() => false;
|
||||
|
||||
protected virtual string GetGoalItemName() => "Victory";
|
||||
protected abstract string GetGoalLocation();
|
||||
protected virtual Cost GetGoalCost() => null;
|
||||
protected virtual UIDef GetGoalUIDef() => new ArchipelagoUIDef()
|
||||
{
|
||||
name = new BoxedString(GetGoalItemName()),
|
||||
shopDesc = new BoxedString("You completed your goal so you should probably get this to flex on your friends."),
|
||||
sprite = new ArchipelagoSprite { key = "IconColorSmall" }
|
||||
};
|
||||
|
||||
public override void OnSelected()
|
||||
{
|
||||
string goalLocation = GetGoalLocation();
|
||||
AbstractPlacement plt = Ref.Settings.Placements.GetOrDefault(goalLocation);
|
||||
if (plt == null)
|
||||
{
|
||||
plt = Finder.GetLocation(goalLocation).Wrap();
|
||||
}
|
||||
|
||||
// don't duplicate the goal
|
||||
if (plt.Items.Any(i => i is GoalItem))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AbstractItem item = new GoalItem()
|
||||
{
|
||||
name = GetGoalItemName(),
|
||||
UIDef = GetGoalUIDef(),
|
||||
};
|
||||
// modules (and therefore goals) are loaded prior to placements so nothing special needed
|
||||
// to make this load.
|
||||
plt.Add(item);
|
||||
|
||||
// handle the cost
|
||||
if (plt is ISingleCostPlacement icsp)
|
||||
{
|
||||
Cost desiredCost = GetGoalCost();
|
||||
if (icsp.Cost == null)
|
||||
{
|
||||
icsp.Cost = desiredCost;
|
||||
}
|
||||
else
|
||||
{
|
||||
icsp.Cost += desiredCost;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
item.AddTag(new CostTag()
|
||||
{
|
||||
Cost = GetGoalCost(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDeselected() { }
|
||||
}
|
||||
|
||||
public class HollowKnightGoal : EndingGoal
|
||||
{
|
||||
public override string Name => "The Hollow Knight";
|
||||
public override string Description => "Defeat The Hollow Knight<br>or any harder ending.";
|
||||
public override string MinimumGoalScene => SceneNames.Cinematic_Ending_A;
|
||||
}
|
||||
|
||||
public class SealedSiblingsGoal : EndingGoal
|
||||
{
|
||||
public override string Name => "Sealed Siblings";
|
||||
public override string Description => "Complete the Sealed Siblings ending<br>or any harder ending.";
|
||||
public override string MinimumGoalScene => SceneNames.Cinematic_Ending_B;
|
||||
}
|
||||
|
||||
public class RadianceGoal : EndingGoal
|
||||
{
|
||||
public override string Name => "Dream No More";
|
||||
public override string Description => "Defeat The Radiance in Black Egg Temple<br>or Absolute Radiance in Pantheon 5.";
|
||||
public override string MinimumGoalScene => SceneNames.Cinematic_Ending_C;
|
||||
}
|
||||
|
||||
public class GodhomeGoal : EndingGoal
|
||||
{
|
||||
public override string Name => "Embrace the Void";
|
||||
public override string Description => "Defeat Absolute Radiance in Pantheon 5.";
|
||||
public override string MinimumGoalScene => "Cinematic_Ending_D";
|
||||
}
|
||||
|
||||
public class GodhomeFlowerGoal : EndingGoal
|
||||
{
|
||||
public override string Name => "Delicate Flower";
|
||||
public override string Description => "Defeat Absolute Radiance in Pantheon 5<br>after delivering the flower to the Godseeker.";
|
||||
public override string MinimumGoalScene => SceneNames.Cinematic_Ending_E;
|
||||
}
|
||||
|
||||
public class GrubHuntGoal : ItemGoal
|
||||
{
|
||||
public override string Name => "Grub Hunt";
|
||||
|
||||
public override string Description => $"Save {ArchipelagoMod.Instance.SlotData.GrubsRequired.Value} of your Grubs and visit Grubfather<br>to obtain happiness.";
|
||||
|
||||
public override bool CanBeSelectedForAnyGoal => ArchipelagoMod.Instance.SlotData.GrubsRequired != null;
|
||||
|
||||
protected override string GetGoalItemName() => "Happiness";
|
||||
protected override string GetGoalLocation() => LocationNames.Grubfather;
|
||||
protected override Cost GetGoalCost() => Cost.NewGrubCost(ArchipelagoMod.Instance.SlotData.GrubsRequired.Value);
|
||||
protected override UIDef GetGoalUIDef() => new ArchipelagoUIDef()
|
||||
{
|
||||
name = new BoxedString("Happiness"),
|
||||
shopDesc = new BoxedString("Meemawmaw! Meemawmaw!"),
|
||||
sprite = new ArchipelagoSprite { key = "GrubHappyv2" }
|
||||
};
|
||||
}
|
||||
}
|
||||
251
Mod/Archipelago.HollowKnight/HintTracker.cs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
using Archipelago.HollowKnight.IC;
|
||||
using Archipelago.HollowKnight.IC.Modules;
|
||||
using Archipelago.MultiClient.Net;
|
||||
using Archipelago.MultiClient.Net.Enums;
|
||||
using Archipelago.MultiClient.Net.Exceptions;
|
||||
using Archipelago.MultiClient.Net.Models;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Modules;
|
||||
using ItemChanger.Placements;
|
||||
using ItemChanger.Tags;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Archipelago.HollowKnight;
|
||||
|
||||
public class HintTracker : Module
|
||||
{
|
||||
|
||||
public static event Action OnArchipelagoHintUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// List of MultiClient.Net Hint's
|
||||
/// </summary>
|
||||
public static List<Hint> Hints;
|
||||
/// <summary>
|
||||
/// List of placement hints to send on scene change or when closing out the session
|
||||
/// </summary>
|
||||
private List<AbstractPlacement> PendingPlacementHints;
|
||||
|
||||
private ArchipelagoSession session;
|
||||
|
||||
private void UpdateHints(Hint[] arrayHints)
|
||||
{
|
||||
|
||||
Hints = arrayHints.ToList();
|
||||
foreach (Hint hint in Hints)
|
||||
{
|
||||
if (hint.FindingPlayer != ArchipelagoMod.Instance.session.ConnectionInfo.Slot)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ArchipelagoPlacementTag.PlacementsByLocationId.ContainsKey(hint.LocationId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ArchipelagoMod.Instance.LogDebug($"Hint data received for item {hint.ItemId} at location {hint.LocationId}");
|
||||
|
||||
AbstractPlacement placement = ArchipelagoPlacementTag.PlacementsByLocationId[hint.LocationId];
|
||||
|
||||
if (placement == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// set the hinted tag for the single item in the placement that was hinted for.
|
||||
foreach (ArchipelagoItemTag tag in placement.Items.Select(item => item.GetTag<ArchipelagoItemTag>())
|
||||
.Where(tag => tag.Location == hint.LocationId))
|
||||
{
|
||||
ArchipelagoMod.Instance.LogDebug("Setting hinted true for item");
|
||||
tag.Hinted = true;
|
||||
}
|
||||
|
||||
// if all items inside a placement have been hinted for then mark the entire placement as hinted.
|
||||
if (placement.Items.TrueForAll(item => item.GetTag<ArchipelagoItemTag>().Hinted))
|
||||
{
|
||||
ArchipelagoMod.Instance.LogDebug("Setting hinted true for placement");
|
||||
placement.GetTag<ArchipelagoPlacementTag>().Hinted = true;
|
||||
}
|
||||
|
||||
if (placement is ShopPlacement shop)
|
||||
{
|
||||
List<(string, AbstractItem)> previewText = new();
|
||||
foreach (AbstractItem item in shop.Items)
|
||||
{
|
||||
if (item.GetTag<ArchipelagoItemTag>().Hinted)
|
||||
{
|
||||
previewText.Add((item.GetPreviewWithCost(), item));
|
||||
}
|
||||
else
|
||||
{
|
||||
previewText.Add((Language.Language.Get("???", "IC"), item));
|
||||
}
|
||||
|
||||
}
|
||||
MultiPreviewRecordTag previewRecordTag = shop.GetOrAddTag<MultiPreviewRecordTag>();
|
||||
previewRecordTag.previewTexts ??= new string[shop.Items.Count];
|
||||
|
||||
foreach ((string, AbstractItem item) p in previewText)
|
||||
{
|
||||
string str = p.Item1;
|
||||
int index = shop.Items.IndexOf(p.item);
|
||||
if (index >= 0)
|
||||
{
|
||||
previewRecordTag.previewTexts[index] = str;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
List<string> previewText = new();
|
||||
foreach (AbstractItem item in placement.Items)
|
||||
{
|
||||
if (item.WasEverObtained())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
previewText.Add(item.GetTag<ArchipelagoItemTag>().Hinted
|
||||
? item.GetPreviewWithCost()
|
||||
: Language.Language.Get("???", "IC"));
|
||||
}
|
||||
|
||||
placement.GetOrAddTag<PreviewRecordTag>().previewText = string.Join(Language.Language.Get("COMMA_SPACE", "IC"), previewText);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
OnArchipelagoHintUpdate?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogError($"Error invoking OnArchipelagoHintUpdate:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
PendingPlacementHints = [];
|
||||
|
||||
session = ArchipelagoMod.Instance.session;
|
||||
|
||||
// do most setup in OnEnterGame so save data can completely load, we need to
|
||||
// populate all the AP placements to sync with server
|
||||
Events.OnEnterGame += OnEnterGame;
|
||||
}
|
||||
|
||||
private void OnEnterGame()
|
||||
{
|
||||
session.DataStorage.TrackHints(UpdateHints);
|
||||
|
||||
AbstractItem.AfterGiveGlobal += UpdateHintFoundStatus;
|
||||
Events.OnSceneChange += SendHintsOnSceneChange;
|
||||
}
|
||||
|
||||
public override async void Unload()
|
||||
{
|
||||
Events.OnEnterGame -= OnEnterGame;
|
||||
AbstractItem.AfterGiveGlobal -= UpdateHintFoundStatus;
|
||||
Events.OnSceneChange -= SendHintsOnSceneChange;
|
||||
await SendPlacementHintsAsync();
|
||||
}
|
||||
|
||||
public void HintPlacement(AbstractPlacement pmt)
|
||||
{
|
||||
// todo - accommodate different hinting times (immediate/never)
|
||||
PendingPlacementHints.Add(pmt);
|
||||
}
|
||||
|
||||
private void UpdateHintFoundStatus(ReadOnlyGiveEventArgs args)
|
||||
{
|
||||
if (Hints != null && args.Orig.GetTag(out ArchipelagoItemTag tag))
|
||||
{
|
||||
long location = tag.Location;
|
||||
foreach (Hint hint in Hints)
|
||||
{
|
||||
if (hint.LocationId == location)
|
||||
{
|
||||
hint.Found = true;
|
||||
try
|
||||
{
|
||||
OnArchipelagoHintUpdate?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogError($"Error invoking OnArchipelagoHintUpdate:\n {ex}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void SendHintsOnSceneChange(Scene scene)
|
||||
{
|
||||
await SendPlacementHintsAsync();
|
||||
}
|
||||
|
||||
private async Task SendPlacementHintsAsync()
|
||||
{
|
||||
if (!PendingPlacementHints.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<ArchipelagoItemTag> hintedTags = new();
|
||||
HashSet<long> hintedLocationIDs = new();
|
||||
ArchipelagoItemTag tag;
|
||||
|
||||
foreach (AbstractPlacement pmt in PendingPlacementHints)
|
||||
{
|
||||
foreach (AbstractItem item in pmt.Items)
|
||||
{
|
||||
if (item.GetTag(out tag) && !tag.Hinted)
|
||||
{
|
||||
if ((tag.Flags.HasFlag(ItemFlags.Advancement) || tag.Flags.HasFlag(ItemFlags.NeverExclude))
|
||||
&& !item.WasEverObtained()
|
||||
&& !item.HasTag<DisableItemPreviewTag>())
|
||||
{
|
||||
hintedTags.Add(tag);
|
||||
hintedLocationIDs.Add(tag.Location);
|
||||
}
|
||||
else
|
||||
{
|
||||
tag.Hinted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PendingPlacementHints.Clear();
|
||||
if (!hintedLocationIDs.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ArchipelagoMod.Instance.LogDebug($"Hinting {hintedLocationIDs.Count()} locations.");
|
||||
try
|
||||
{
|
||||
await session.Locations.ScoutLocationsAsync(true, hintedLocationIDs.ToArray())
|
||||
.ContinueWith(x =>
|
||||
{
|
||||
bool result = !x.IsFaulted;
|
||||
foreach (ArchipelagoItemTag tag in hintedTags)
|
||||
{
|
||||
tag.Hinted = result;
|
||||
}
|
||||
}).TimeoutAfter(1000);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArchipelagoSocketClosedException or TimeoutException)
|
||||
{
|
||||
ItemChangerMod.Modules.Get<ItemNetworkingModule>().ReportDisconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
89
Mod/Archipelago.HollowKnight/IC/ArchipelagoItem.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
using Archipelago.MultiClient.Net.Enums;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Tags;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC
|
||||
{
|
||||
public class ArchipelagoDummyItem : AbstractItem
|
||||
{
|
||||
public string PreferredContainerType { get; set; } = Container.Unknown;
|
||||
|
||||
public override string GetPreferredContainer() => PreferredContainerType;
|
||||
|
||||
public ArchipelagoDummyItem()
|
||||
{ }
|
||||
public ArchipelagoDummyItem(AbstractItem source)
|
||||
{
|
||||
this.name = source.name;
|
||||
this.UIDef = source.UIDef.Clone();
|
||||
PreferredContainerType = source.GetPreferredContainer();
|
||||
}
|
||||
|
||||
public override bool GiveEarly(string containerType)
|
||||
{
|
||||
// any container (e.g. a grub or soul totem) that would not normally fling a shiny
|
||||
// in vanilla should not go out of its way to do so for this
|
||||
return containerType switch
|
||||
{
|
||||
Container.Unknown
|
||||
or Container.Shiny
|
||||
or Container.Chest
|
||||
=> false,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
public override void GiveImmediate(GiveInfo info)
|
||||
{
|
||||
// Intentional no-op
|
||||
}
|
||||
}
|
||||
|
||||
public class ArchipelagoItem : AbstractItem
|
||||
{
|
||||
public ArchipelagoItem(string name, string recipientName = null, ItemFlags itemFlags = 0)
|
||||
{
|
||||
string desc;
|
||||
ISprite pinSprite;
|
||||
if (itemFlags.HasFlag(ItemFlags.Advancement))
|
||||
{
|
||||
desc = "This otherworldly artifact looks very important. Somebody probably really needs it.";
|
||||
pinSprite = new ArchipelagoSprite { key = "Pins.pinAPProgression" };
|
||||
}
|
||||
else if (itemFlags.HasFlag(ItemFlags.NeverExclude))
|
||||
{
|
||||
desc = "This otherworldly artifact looks like it might be useful to someone.";
|
||||
pinSprite = new ArchipelagoSprite { key = "Pins.pinAPUseful" };
|
||||
}
|
||||
else
|
||||
{
|
||||
desc = "I'm not entirely sure what this is. It appears to be a strange artifact from another world.";
|
||||
pinSprite = new ArchipelagoSprite { key = "Pins.pinAP" };
|
||||
}
|
||||
if (itemFlags.HasFlag(ItemFlags.Trap))
|
||||
{
|
||||
desc += " Seems kinda suspicious though. It might be full of bees.";
|
||||
}
|
||||
this.name = name;
|
||||
UIDef = new ArchipelagoUIDef()
|
||||
{
|
||||
name = new BoxedString($"{recipientName}'s {name}"),
|
||||
shopDesc = new BoxedString(desc),
|
||||
sprite = new ArchipelagoSprite { key = "IconColorSmall" }
|
||||
};
|
||||
InteropTag mapInteropTag = new()
|
||||
{
|
||||
Message = "RandoSupplementalMetadata",
|
||||
Properties = new() {
|
||||
["PinSprite"] = pinSprite
|
||||
}
|
||||
};
|
||||
this.AddTag(mapInteropTag);
|
||||
}
|
||||
|
||||
public override void GiveImmediate(GiveInfo info)
|
||||
{
|
||||
// intentional no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Mod/Archipelago.HollowKnight/IC/ArchipelagoSprite.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using ItemChanger;
|
||||
using ItemChanger.Internal;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC
|
||||
{
|
||||
public class ArchipelagoSprite : EmbeddedSprite
|
||||
{
|
||||
public override SpriteManager SpriteManager => ArchipelagoMod.Instance.spriteManager;
|
||||
}
|
||||
}
|
||||
151
Mod/Archipelago.HollowKnight/IC/ArchipelagoTags.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
using Archipelago.HollowKnight.IC.Modules;
|
||||
using Archipelago.MultiClient.Net.Enums;
|
||||
using Archipelago.MultiClient.Net.Models;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Tags;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC
|
||||
{
|
||||
/// <summary>
|
||||
/// Tag attached to items that are involved with Archipelago.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ArchipelagoItemTags are attached to AP-randomized items to track what their location ID and player ("slot") are. Additionally, they manage events
|
||||
/// for when items are picked up, ensuring that HK items for other players get replaced out and that all location checks actually get sent.
|
||||
/// </remarks>
|
||||
public class ArchipelagoItemTag : Tag, IInteropTag
|
||||
{
|
||||
/// <summary>
|
||||
/// AP location ID for this item.
|
||||
/// </summary>
|
||||
public long Location { get; set; }
|
||||
/// <summary>
|
||||
/// AP player ID ("slot") for this item's recipient.
|
||||
/// </summary>
|
||||
public int Player { get; set; }
|
||||
/// <summary>
|
||||
/// Network item flags, exposed for benefit of the mapmod
|
||||
/// </summary>
|
||||
public ItemFlags Flags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set if this area is hinted.
|
||||
/// </summary>
|
||||
public bool Hinted { get; set; } = false;
|
||||
|
||||
public bool IsItemForMe { get; set; }
|
||||
|
||||
private ItemNetworkingModule networkModule;
|
||||
|
||||
public void ReadItemInfo(ScoutedItemInfo itemInfo)
|
||||
{
|
||||
Location = itemInfo.LocationId;
|
||||
Player = itemInfo.Player;
|
||||
Flags = itemInfo.Flags;
|
||||
|
||||
IsItemForMe = itemInfo.IsReceiverRelatedToActivePlayer;
|
||||
}
|
||||
|
||||
public override async void Load(object parent)
|
||||
{
|
||||
base.Load(parent);
|
||||
networkModule = ItemChangerMod.Modules.Get<ItemNetworkingModule>();
|
||||
AbstractItem item = (AbstractItem)parent;
|
||||
item.AfterGive += AfterGive;
|
||||
|
||||
if (item.WasEverObtained())
|
||||
{
|
||||
await networkModule.SendLocationsAsync(Location);
|
||||
}
|
||||
}
|
||||
|
||||
private async void AfterGive(ReadOnlyGiveEventArgs obj)
|
||||
{
|
||||
await networkModule.SendLocationsAsync(Location);
|
||||
}
|
||||
|
||||
public override void Unload(object parent)
|
||||
{
|
||||
((AbstractItem)parent).AfterGive -= AfterGive;
|
||||
base.Unload(parent);
|
||||
}
|
||||
|
||||
string IInteropTag.Message => "RandoSupplementalMetadata";
|
||||
|
||||
bool IInteropTag.TryGetProperty<T>(string propertyName, out T value) where T : default
|
||||
{
|
||||
if (propertyName == "ForceEnablePreview" && Hinted is T t)
|
||||
{
|
||||
value = t;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tag attached to placements that are involved with Archipelago.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ArchipelagoPlacementTags are attached to placements containing AP-randomized.
|
||||
/// 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
|
||||
/// 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
|
||||
/// world accordingly.
|
||||
/// </remarks>
|
||||
public class ArchipelagoPlacementTag : Tag
|
||||
{
|
||||
public static Dictionary<long, AbstractPlacement> PlacementsByLocationId = new();
|
||||
|
||||
/// <summary>
|
||||
/// True if this location has been hinted AP, or is in the process of being hinted.
|
||||
/// </summary>
|
||||
public bool Hinted { get; set; }
|
||||
|
||||
private HintTracker hintTracker;
|
||||
|
||||
public override void Load(object parent)
|
||||
{
|
||||
base.Load(parent);
|
||||
AbstractPlacement pmt = (AbstractPlacement)parent;
|
||||
//Archipelago.Instance.LogDebug($"In ArchipelagoPlacementTag:Load for {parent}, locations ({String.Join(", ", PlacementUtils.GetLocationIDs(pmt))})");
|
||||
hintTracker = ItemChangerMod.Modules.Get<HintTracker>();
|
||||
|
||||
foreach (long locationId in PlacementUtils.GetLocationIDs(pmt))
|
||||
{
|
||||
PlacementsByLocationId[locationId] = pmt;
|
||||
}
|
||||
|
||||
pmt.OnVisitStateChanged += OnVisitStateChanged;
|
||||
// If we've been previewed but never told AP that, tell it now
|
||||
if (!Hinted && pmt.Visited.HasFlag(VisitState.Previewed))
|
||||
{
|
||||
hintTracker.HintPlacement(pmt);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unload(object parent)
|
||||
{
|
||||
//Archipelago.Instance.LogDebug($"In ArchipelagoPlacementTag:UNLOAD for {parent}, locations ({String.Join(", ", PlacementUtils.GetLocationIDs((AbstractPlacement)parent))})");
|
||||
((AbstractPlacement)parent).OnVisitStateChanged -= OnVisitStateChanged;
|
||||
|
||||
foreach (long locationId in PlacementUtils.GetLocationIDs((AbstractPlacement)parent))
|
||||
{
|
||||
PlacementsByLocationId.Remove(locationId);
|
||||
}
|
||||
|
||||
base.Unload(parent);
|
||||
}
|
||||
|
||||
private void OnVisitStateChanged(VisitStateChangedEventArgs obj)
|
||||
{
|
||||
if (!Hinted && obj.NewFlags.HasFlag(VisitState.Previewed))
|
||||
{
|
||||
// We are now previewed, but we weren't before.
|
||||
hintTracker.HintPlacement(obj.Placement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Mod/Archipelago.HollowKnight/IC/ArchipelagoUIDef.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using ItemChanger;
|
||||
using ItemChanger.UIDefs;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC
|
||||
{
|
||||
internal class ArchipelagoUIDef : MsgUIDef
|
||||
{
|
||||
public static string GetSentItemName(AbstractItem item)
|
||||
{
|
||||
return item.name switch
|
||||
{
|
||||
ItemNames.Grub => "A grub!",
|
||||
ItemNames.Grimmkin_Flame => "Grimmkin Flame",
|
||||
ItemNames.Rancid_Egg => "Rancid Egg",
|
||||
_ => item.UIDef.GetPostviewName(),
|
||||
};
|
||||
}
|
||||
|
||||
public static ArchipelagoUIDef CreateForReceivedItem(AbstractItem item, string sender)
|
||||
{
|
||||
return CreateForReceivedItem(item.GetResolvedUIDef(), sender);
|
||||
}
|
||||
|
||||
public static ArchipelagoUIDef CreateForReceivedItem(UIDef source, string sender)
|
||||
{
|
||||
ArchipelagoUIDef result = new(source);
|
||||
result.name = new BoxedString($"{source.GetPostviewName()} from {sender}");
|
||||
return result;
|
||||
}
|
||||
public static ArchipelagoUIDef CreateForSentItem(AbstractItem item, string recipient)
|
||||
{
|
||||
ArchipelagoUIDef result = new(item.UIDef);
|
||||
result.name = new BoxedString($"{recipient}'s {GetSentItemName(item)}");
|
||||
return result;
|
||||
}
|
||||
|
||||
internal ArchipelagoUIDef() : base()
|
||||
{
|
||||
}
|
||||
|
||||
internal ArchipelagoUIDef(UIDef source) : base()
|
||||
{
|
||||
if (source is MsgUIDef msgDef)
|
||||
{
|
||||
shopDesc = msgDef.shopDesc.Clone();
|
||||
sprite = msgDef.sprite.Clone();
|
||||
}
|
||||
else
|
||||
{
|
||||
shopDesc = new BoxedString(source.GetShopDesc());
|
||||
sprite = new EmptySprite();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
Mod/Archipelago.HollowKnight/IC/CostFactory.cs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
using ItemChanger;
|
||||
using ItemChanger.Placements;
|
||||
using ItemChanger.Tags;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC
|
||||
{
|
||||
internal class CostFactory
|
||||
{
|
||||
private Dictionary<string, Dictionary<string, int>> locationCosts;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a cost factory with costs provided from slot data
|
||||
/// </summary>
|
||||
/// <param name="locationCosts">A lookup from placement name -> cost type -> amount</param>
|
||||
public CostFactory(Dictionary<string, Dictionary<string, int>> locationCosts)
|
||||
{
|
||||
this.locationCosts = locationCosts;
|
||||
}
|
||||
|
||||
public void ApplyCost(AbstractPlacement pmt, AbstractItem item, string serverLocationName)
|
||||
{
|
||||
if (locationCosts.TryGetValue(serverLocationName, out Dictionary<string, int> costs))
|
||||
{
|
||||
List<Cost> icCosts = new();
|
||||
foreach (KeyValuePair<string, int> entry in costs)
|
||||
{
|
||||
Cost proposedCost = null;
|
||||
switch (entry.Key)
|
||||
{
|
||||
case "GEO":
|
||||
proposedCost = Cost.NewGeoCost(entry.Value);
|
||||
break;
|
||||
case "ESSENCE":
|
||||
proposedCost = Cost.NewEssenceCost(entry.Value);
|
||||
break;
|
||||
case "GRUBS":
|
||||
proposedCost = Cost.NewGrubCost(entry.Value);
|
||||
break;
|
||||
case "CHARMS":
|
||||
proposedCost = new PDIntCost(
|
||||
entry.Value, nameof(PlayerData.charmsOwned),
|
||||
$"Acquire {entry.Value} {((entry.Value == 1) ? "charm" : "charms")}"
|
||||
);
|
||||
break;
|
||||
case "RANCIDEGGS":
|
||||
proposedCost = new ItemChanger.Modules.CumulativeRancidEggCost(entry.Value);
|
||||
break;
|
||||
default:
|
||||
ArchipelagoMod.Instance.LogWarn(
|
||||
$"Encountered UNKNOWN currency type {entry.Key} at location {serverLocationName}!");
|
||||
break;
|
||||
}
|
||||
|
||||
if (proposedCost != null)
|
||||
{
|
||||
// suppress inherent costs - if the server told us to pay X, but the implementation of
|
||||
// the location will force us to pay Y >= X, we skip adding the cost to prevent doubling up.
|
||||
IEnumerable<Cost> inherentCosts = pmt.GetPlacementAndLocationTags()
|
||||
.OfType<ImplicitCostTag>()
|
||||
.Where(t => t.Inherent)
|
||||
.Select(t => t.Cost);
|
||||
if (inherentCosts.Any(c => c.Includes(proposedCost)))
|
||||
{
|
||||
ArchipelagoMod.Instance.LogDebug($"Supressing cost {entry.Value} {entry.Key} for location {serverLocationName}");
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
icCosts.Add(proposedCost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (icCosts.Count == 0)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogWarn(
|
||||
$"Found zero cost types when handling placement at location {serverLocationName}!");
|
||||
return;
|
||||
}
|
||||
|
||||
Cost finalCosts;
|
||||
if (icCosts.Count == 1)
|
||||
{
|
||||
finalCosts = icCosts[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
finalCosts = new MultiCost(icCosts);
|
||||
}
|
||||
|
||||
if (pmt is ISingleCostPlacement scp)
|
||||
{
|
||||
if (scp.Cost == null)
|
||||
{
|
||||
scp.Cost = finalCosts;
|
||||
}
|
||||
else
|
||||
{
|
||||
scp.Cost = new MultiCost(scp.Cost, finalCosts);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CostTag costTag = item.AddTag<CostTag>();
|
||||
costTag.Cost = finalCosts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Mod/Archipelago.HollowKnight/IC/ItemFactory.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using Archipelago.MultiClient.Net;
|
||||
using Archipelago.MultiClient.Net.Models;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Items;
|
||||
using ItemChanger.Tags;
|
||||
using System;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC
|
||||
{
|
||||
internal class ItemFactory
|
||||
{
|
||||
public AbstractItem CreateMyItem(string itemName, ScoutedItemInfo itemInfo)
|
||||
{
|
||||
AbstractItem item = Finder.GetItem(itemName);
|
||||
if (item == null)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogError($"Could not find local item with name {itemName}");
|
||||
throw new NullReferenceException($"Could not find local item with name {itemName}");
|
||||
}
|
||||
|
||||
AddArchipelagoTag(item, itemInfo);
|
||||
return item;
|
||||
}
|
||||
|
||||
public AbstractItem CreateRemoteItem(AbstractPlacement targetPlacement, string slotName, string itemName, ScoutedItemInfo itemInfo)
|
||||
{
|
||||
ArchipelagoSession session = ArchipelagoMod.Instance.session;
|
||||
string game = itemInfo.ItemGame;
|
||||
|
||||
AbstractItem orig = Finder.GetItem(itemName);
|
||||
AbstractItem item;
|
||||
if (game == "Hollow Knight" && orig != null)
|
||||
{
|
||||
// this is a remote HK item - make it a no-op, but cosmetically correct
|
||||
item = new ArchipelagoDummyItem(orig);
|
||||
item.UIDef = ArchipelagoUIDef.CreateForSentItem(orig, slotName);
|
||||
|
||||
// give the placement the correct cosmetic soul totem or geo rock type if appropriate
|
||||
if (orig is SoulTotemItem totem)
|
||||
{
|
||||
targetPlacement.GetOrAddTag<SoulTotemSubtypeTag>().Type = totem.soulTotemSubtype;
|
||||
}
|
||||
else if (orig is GeoRockItem rock)
|
||||
{
|
||||
targetPlacement.GetOrAddTag<GeoRockSubtypeTag>().Type = rock.geoRockSubtype;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Items from other games, or an unknown HK item
|
||||
item = new ArchipelagoItem(itemName, slotName, itemInfo.Flags);
|
||||
}
|
||||
|
||||
AddArchipelagoTag(item, itemInfo);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void AddArchipelagoTag(AbstractItem item, ScoutedItemInfo itemInfo)
|
||||
{
|
||||
ArchipelagoItemTag itemTag = item.AddTag<ArchipelagoItemTag>();
|
||||
itemTag.ReadItemInfo(itemInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Mod/Archipelago.HollowKnight/IC/Items/GoalItem.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using Archipelago.HollowKnight.IC.Modules;
|
||||
using ItemChanger;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.Items
|
||||
{
|
||||
public class GoalItem : AbstractItem
|
||||
{
|
||||
private GoalModule goalModule;
|
||||
|
||||
protected override void OnLoad()
|
||||
{
|
||||
goalModule = ItemChangerMod.Modules.Get<GoalModule>();
|
||||
}
|
||||
|
||||
public override async void GiveImmediate(GiveInfo info)
|
||||
{
|
||||
await goalModule.DeclareVictoryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
using Archipelago.MultiClient.Net.Models;
|
||||
using ItemChanger.Modules;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.Modules;
|
||||
|
||||
public class ArchipelagoRemoteItemCounterModule : Module
|
||||
{
|
||||
/// <summary>
|
||||
/// Full history of remote items received and saved by the client. Keys are player, location, item to finally reach a count.
|
||||
/// </summary>
|
||||
private readonly Dictionary<int, Dictionary<long, Dictionary<long, int>>> savedItemCounts = new();
|
||||
|
||||
/// <summary>
|
||||
/// History of items seen when receiving from server. Keys are player, location, item to finally reach a count.
|
||||
/// </summary>
|
||||
|
||||
private readonly Dictionary<int, Dictionary<long, Dictionary<long, int>>> serverSeenItemCounts = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
}
|
||||
|
||||
public override void Unload()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified item should be received from the server based on the current counts. Should be called before receiving the item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to evaluate for receiving from the server.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if receiving the item would result in the server count exceeding the local saved count;
|
||||
/// otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
public bool ShouldReceiveServerItem(ItemInfo item)
|
||||
{
|
||||
int currentSavedCount = EnsureCountExists(item.Player, item.LocationId, item.ItemId, savedItemCounts);
|
||||
int currentServerCount = EnsureCountExists(item.Player, item.LocationId, item.ItemId, serverSeenItemCounts);
|
||||
|
||||
// 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.
|
||||
return currentServerCount + 1 > currentSavedCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increments the server-side count for the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The <see cref="ItemInfo"/> object representing the item whose count is to be incremented. This includes details
|
||||
/// such as the player, location, and item identifier.</param>
|
||||
public void IncrementServerCountForItem(ItemInfo item)
|
||||
{
|
||||
EnsureCountExists(item.Player, item.LocationId, item.ItemId, serverSeenItemCounts);
|
||||
IncrementCurrentCountForItem(item.Player, item.LocationId, item.ItemId, serverSeenItemCounts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increments the saved count for a specific item at a given location for a specified player.
|
||||
/// </summary>
|
||||
/// <param name="player">The identifier of the player for whom the item's saved count is being incremented.</param>
|
||||
/// <param name="locationId">The identifier of the location where the item is stored.</param>
|
||||
/// <param name="itemId">The identifier of the item whose saved count is being incremented.</param>
|
||||
public void IncrementSavedCountForItem(int player, long locationId, long itemId)
|
||||
{
|
||||
EnsureCountExists(player, locationId, itemId, savedItemCounts);
|
||||
IncrementCurrentCountForItem(player, locationId, itemId, savedItemCounts);
|
||||
}
|
||||
|
||||
private static void IncrementCurrentCountForItem(int player, long locationId, long itemId, Dictionary<int, Dictionary<long, Dictionary<long, int>>> itemCounts, int incrementBy = 1)
|
||||
{
|
||||
itemCounts[player][locationId][itemId] += incrementBy;
|
||||
}
|
||||
|
||||
private static int EnsureCountExists(int player, long locationId, long itemId, Dictionary<int, Dictionary<long, Dictionary<long, int>>> itemCounts)
|
||||
{
|
||||
if (!itemCounts.TryGetValue(player, out Dictionary<long, Dictionary<long, int>> a))
|
||||
{
|
||||
itemCounts[player] = a = new();
|
||||
}
|
||||
|
||||
if (!a.TryGetValue(locationId, out Dictionary<long, int> b))
|
||||
{
|
||||
a[locationId] = b = new();
|
||||
}
|
||||
|
||||
if (!b.TryGetValue(itemId, out int count))
|
||||
{
|
||||
b[itemId] = count = 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
123
Mod/Archipelago.HollowKnight/IC/Modules/BenchSyncModule.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
using Archipelago.MultiClient.Net;
|
||||
using Archipelago.MultiClient.Net.Enums;
|
||||
using Archipelago.MultiClient.Net.Models;
|
||||
using Benchwarp;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Modules;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.Modules
|
||||
{
|
||||
public partial class BenchSyncModule : Module
|
||||
{
|
||||
private const string DATASTORAGE_KEY_UNLOCKED_BENCHES = "unlocked_benches";
|
||||
private const string BENCH_KEY_SEPARATOR = ":::";
|
||||
static readonly Dictionary<string, string> lockedBenches = new()
|
||||
{
|
||||
[SceneNames.Hive_01] = "Hive Bench",
|
||||
[SceneNames.Ruins1_31] = "Toll Machine Bench",
|
||||
[SceneNames.Abyss_18] = "Toll Machine Bench",
|
||||
[SceneNames.Fungus3_50] = "Toll Machine Bench"
|
||||
};
|
||||
|
||||
private ArchipelagoSession session;
|
||||
|
||||
private Dictionary<BenchKey, Bench> benchLookup;
|
||||
|
||||
[DataStorageProperty(nameof(session), Scope.Slot, DATASTORAGE_KEY_UNLOCKED_BENCHES)]
|
||||
private readonly DataStorageElement _unlockedBenches;
|
||||
|
||||
public override async void Initialize()
|
||||
{
|
||||
session = ArchipelagoMod.Instance.session;
|
||||
benchLookup = Bench.Benches.ToDictionary(x => x.ToBenchKey(), x => x);
|
||||
|
||||
Benchwarp.Events.OnBenchUnlock += OnUnlockLocalBench;
|
||||
UnlockedBenches.Initialize(JObject.FromObject(new Dictionary<string, bool>()));
|
||||
|
||||
UnlockedBenches.OnValueChanged += OnUnlockRemoteBench;
|
||||
Dictionary<string, bool> benchData = BuildBenchData(Bench.Benches.Where(x => x.HasVisited()).Select(x => x.ToBenchKey()));
|
||||
UnlockedBenches += Operation.Update(benchData);
|
||||
|
||||
try
|
||||
{
|
||||
Dictionary<string, bool> benches = await UnlockedBenches.GetAsync<Dictionary<string, bool>>();
|
||||
UnlockBenches(benches);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogError($"Unexpected issue unlocking benches from server data: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unload()
|
||||
{
|
||||
Benchwarp.Events.OnBenchUnlock -= OnUnlockLocalBench;
|
||||
UnlockedBenches.OnValueChanged -= OnUnlockRemoteBench;
|
||||
}
|
||||
|
||||
private void OnUnlockLocalBench(BenchKey obj)
|
||||
{
|
||||
UnlockedBenches += Operation.Update(BuildBenchData([obj]));
|
||||
}
|
||||
|
||||
private void OnUnlockRemoteBench(JToken oldData, JToken newData, Dictionary<string, JToken> args)
|
||||
{
|
||||
Dictionary<string, bool> benches = newData.ToObject<Dictionary<string, bool>>();
|
||||
UnlockBenches(benches);
|
||||
}
|
||||
|
||||
private Dictionary<string, bool> BuildBenchData(IEnumerable<BenchKey> keys)
|
||||
{
|
||||
Dictionary<string, bool> obtainedBenches = new();
|
||||
foreach (BenchKey key in keys)
|
||||
{
|
||||
obtainedBenches[$"{key.SceneName}{BENCH_KEY_SEPARATOR}{key.RespawnMarkerName}"] = true;
|
||||
}
|
||||
return obtainedBenches;
|
||||
}
|
||||
|
||||
private void UnlockBenches(Dictionary<string, bool> benches)
|
||||
{
|
||||
if (benches == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, bool> kv in benches)
|
||||
{
|
||||
string[] keyParts = kv.Key.Split([BENCH_KEY_SEPARATOR], StringSplitOptions.None);
|
||||
BenchKey key = new(keyParts[0], keyParts[1]);
|
||||
if (benchLookup.TryGetValue(key, out Bench bench))
|
||||
{
|
||||
bench.SetVisited(kv.Value);
|
||||
if (lockedBenches.TryGetValue(bench.sceneName, out string persistentBoolName))
|
||||
{
|
||||
GameManager.instance.sceneData.SaveMyState(new PersistentBoolData()
|
||||
{
|
||||
activated = true,
|
||||
sceneName = bench.sceneName,
|
||||
semiPersistent = false,
|
||||
id = persistentBoolName
|
||||
});
|
||||
}
|
||||
|
||||
switch (bench.sceneName)
|
||||
{
|
||||
case SceneNames.Room_Tram:
|
||||
PlayerData.instance.SetBool(nameof(PlayerData.openedTramLower), true);
|
||||
PlayerData.instance.SetBool(nameof(PlayerData.tramOpenedDeepnest), true);
|
||||
break;
|
||||
case SceneNames.Room_Tram_RG:
|
||||
PlayerData.instance.SetBool(nameof(PlayerData.openedTramRestingGrounds), true);
|
||||
PlayerData.instance.SetBool(nameof(PlayerData.tramOpenedCrossroads), true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
266
Mod/Archipelago.HollowKnight/IC/Modules/DeathLinkModule.cs
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
using Archipelago.MultiClient.Net;
|
||||
using Archipelago.MultiClient.Net.BounceFeatures.DeathLink;
|
||||
using HutongGames.PlayMaker;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Extensions;
|
||||
using ItemChanger.FsmStateActions;
|
||||
using Modding;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.Modules
|
||||
{
|
||||
public class DeathLinkModule : ItemChanger.Modules.Module
|
||||
{
|
||||
public const string PREVENT_SHADE_VARIABLE_NAME = "Deathlink Prevent Shade";
|
||||
public const string IS_DEATHLINK_VARIABLE_NAME = "Is Deathlink Death";
|
||||
private static readonly MethodInfo HeroController_CanTakeDamage = typeof(HeroController)
|
||||
.GetMethod("CanTakeDamage", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
private DeathLinkService service = null;
|
||||
private DeathLinkShadeHandling shadeHandling => ArchipelagoMod.Instance.SlotData.Options.DeathLinkShade;
|
||||
private bool breakFragileCharms => ArchipelagoMod.Instance.SlotData.Options.DeathLinkBreaksFragileCharms;
|
||||
private DeathLinkStatus status;
|
||||
private int lastDamageType;
|
||||
private DateTime lastDamageTime;
|
||||
private bool hasEditedFsm = false;
|
||||
|
||||
private ArchipelagoSession session;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
lastDamageType = 0;
|
||||
lastDamageTime = DateTime.MinValue;
|
||||
status = DeathLinkStatus.None;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
session = ArchipelagoMod.Instance.session;
|
||||
Reset();
|
||||
|
||||
ArchipelagoMod.Instance.LogDebug($"Enabling DeathLink support, type: {shadeHandling}");
|
||||
service = ArchipelagoMod.Instance.session.CreateDeathLinkService();
|
||||
service.EnableDeathLink();
|
||||
service.OnDeathLinkReceived += OnDeathLinkReceived;
|
||||
ModHooks.HeroUpdateHook += OnHeroUpdate;
|
||||
On.HeroController.TakeDamage += OnTakeDamage;
|
||||
Events.AddFsmEdit(new FsmID("Hero Death Anim"), FsmEdit);
|
||||
}
|
||||
|
||||
public override void Unload()
|
||||
{
|
||||
Reset();
|
||||
|
||||
if (service != null)
|
||||
{
|
||||
service.OnDeathLinkReceived -= OnDeathLinkReceived;
|
||||
service = null;
|
||||
}
|
||||
|
||||
ModHooks.HeroUpdateHook -= OnHeroUpdate;
|
||||
On.HeroController.TakeDamage -= OnTakeDamage;
|
||||
Events.RemoveFsmEdit(new FsmID("Hero Death Anim"), FsmEdit);
|
||||
hasEditedFsm = false;
|
||||
}
|
||||
|
||||
private void FsmEdit(PlayMakerFSM fsm)
|
||||
{
|
||||
if (hasEditedFsm)
|
||||
{
|
||||
return;
|
||||
}
|
||||
hasEditedFsm = true;
|
||||
|
||||
ArchipelagoMod ap = ArchipelagoMod.Instance;
|
||||
|
||||
FsmBool preventShade = fsm.AddFsmBool(PREVENT_SHADE_VARIABLE_NAME, false);
|
||||
FsmBool isDeathlink = fsm.AddFsmBool(IS_DEATHLINK_VARIABLE_NAME, false);
|
||||
// Death animation starts here - normally whether you get a shade or not is determined purely by whether
|
||||
// you're in a dream or not.
|
||||
FsmState mapZone = fsm.GetState("Map Zone");
|
||||
|
||||
// If it's not someone else's death, then send out a deathlink. Also compute whether a shade should be spawned since
|
||||
// multiple other states need to know.
|
||||
mapZone.AddFirstAction(new Lambda(() =>
|
||||
{
|
||||
ap.LogDebug($"FsmEdit Pre: Status={status} Shade handling={shadeHandling} Break fragiles={breakFragileCharms}.");
|
||||
|
||||
bool isDeathlinkDeath = status == DeathLinkStatus.Dying;
|
||||
|
||||
if (!isDeathlinkDeath)
|
||||
{
|
||||
ap.LogDebug($"FsmEdit Pre: Not a deathlink death, so sending out our own deathlink.");
|
||||
// If we're not caused by DeathLink... then we send a DeathLink
|
||||
SendDeathLink();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
ap.LogDebug("Beginning deathlink death handling");
|
||||
}
|
||||
|
||||
isDeathlink.Value = isDeathlinkDeath;
|
||||
preventShade.Value = !(
|
||||
shadeHandling == DeathLinkShadeHandling.Vanilla
|
||||
|| shadeHandling == DeathLinkShadeHandling.Shade && PlayerData.instance.shadeScene == "None"
|
||||
);
|
||||
|
||||
if (!preventShade.Value)
|
||||
{
|
||||
ap.LogDebug($"FsmEdit Pre: Shade will be created.");
|
||||
}
|
||||
}));
|
||||
|
||||
// route around penalties based on settings
|
||||
FsmState breakMsg = fsm.GetState("Break Msg");
|
||||
FsmState removeOvercharm = fsm.GetState("Remove Overcharm");
|
||||
|
||||
FsmState createShadeCheck = fsm.AddState("Create Shade?");
|
||||
createShadeCheck.AddLastAction(new DelegateBoolTest(() => isDeathlink.Value, null, "FINISHED"));
|
||||
createShadeCheck.AddLastAction(new DelegateBoolTest(() => preventShade.Value, "SKIP SHADE", null));
|
||||
createShadeCheck.AddTransition("SKIP SHADE", "Save");
|
||||
createShadeCheck.AddTransition("FINISHED", "Remove Geo");
|
||||
|
||||
FsmState breakFragilesCheck = fsm.AddState("Break Fragiles?");
|
||||
breakFragilesCheck.AddLastAction(new DelegateBoolTest(() => isDeathlink.Value, null, "FINISHED"));
|
||||
breakFragilesCheck.AddLastAction(new DelegateBoolTest(() => !breakFragileCharms, "SKIP BREAK", null));
|
||||
breakFragilesCheck.AddTransition("SKIP BREAK", createShadeCheck);
|
||||
breakFragilesCheck.AddTransition("FINISHED", "Break Glass HP");
|
||||
|
||||
mapZone.RemoveTransitionsOn("FINISHED");
|
||||
mapZone.AddTransition("FINISHED", breakFragilesCheck);
|
||||
|
||||
breakMsg.RemoveTransitionsOn("FINISHED");
|
||||
breakMsg.AddTransition("FINISHED", createShadeCheck);
|
||||
removeOvercharm.RemoveTransitionsOn("FINISHED");
|
||||
removeOvercharm.AddTransition("FINISHED", createShadeCheck);
|
||||
|
||||
// adjust soul limiter to be created only if a shade was created
|
||||
FsmState deathEnding = fsm.GetState("End");
|
||||
fsm.GetState("Limit Soul?").Actions = [];
|
||||
// Replace the first two action (which normally start the soul limiter and notify about it)
|
||||
deathEnding.Actions[0] = new Lambda(() =>
|
||||
{
|
||||
// Mimic the Limit Soul? state and the action being replaced - we only want to soul limit if the
|
||||
// player spawned a shade
|
||||
if (!preventShade.Value)
|
||||
{
|
||||
fsm.Fsm.BroadcastEvent("SOUL LIMITER UP");
|
||||
GameManager.instance.StartSoulLimiter();
|
||||
}
|
||||
});
|
||||
|
||||
// the following 3 states are the ending states of each branch of the FSM. we'll link them into a custom state that resets
|
||||
// deathlink for us
|
||||
FsmState dreamReturn = fsm.GetState("Dream Return");
|
||||
FsmState waitForHeroController = fsm.GetState("Wait for HeroController");
|
||||
FsmState steelSoulCheck = fsm.GetState("Shade?");
|
||||
FsmState[] endingStates = [dreamReturn, waitForHeroController, steelSoulCheck];
|
||||
// add deathlink cleanup state
|
||||
FsmState cleanupDeathlink = fsm.AddState("Cleanup Deathlink");
|
||||
cleanupDeathlink.AddFirstAction(new Lambda(() =>
|
||||
{
|
||||
ap.LogDebug("Resetting deathlink state");
|
||||
preventShade.Value = false;
|
||||
isDeathlink.Value = false;
|
||||
status = DeathLinkStatus.None;
|
||||
}));
|
||||
foreach (FsmState state in endingStates)
|
||||
{
|
||||
state.AddTransition("FINISHED", cleanupDeathlink);
|
||||
}
|
||||
}
|
||||
|
||||
public void MurderPlayer()
|
||||
{
|
||||
string scene = GameManager.instance.sceneName;
|
||||
ArchipelagoMod.Instance.LogDebug($"Deathlink-initiated kill starting. Current scene: {scene}");
|
||||
status = DeathLinkStatus.Dying;
|
||||
HeroController.instance.TakeDamage(HeroController.instance.gameObject, GlobalEnums.CollisionSide.other,
|
||||
9999, 0);
|
||||
}
|
||||
|
||||
private void OnHeroUpdate()
|
||||
{
|
||||
HeroController hc = HeroController.instance;
|
||||
if (status == DeathLinkStatus.Pending
|
||||
&& hc.acceptingInput
|
||||
&& hc.damageMode == GlobalEnums.DamageMode.FULL_DAMAGE
|
||||
&& PlayerData.instance.GetInt(nameof(PlayerData.health)) > 0
|
||||
&& (bool)HeroController_CanTakeDamage.Invoke(hc, null))
|
||||
{
|
||||
MurderPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTakeDamage(On.HeroController.orig_TakeDamage orig, HeroController self,
|
||||
UnityEngine.GameObject go, GlobalEnums.CollisionSide damageSide, int damageAmount, int hazardType)
|
||||
{
|
||||
lastDamageTime = DateTime.UtcNow;
|
||||
lastDamageType = hazardType;
|
||||
orig(self, go, damageSide, damageAmount, hazardType);
|
||||
}
|
||||
|
||||
public void SendDeathLink()
|
||||
{
|
||||
ArchipelagoMod ap = ArchipelagoMod.Instance;
|
||||
// Don't send death links if we're currently in the process of dying to another deathlink.
|
||||
if (status == DeathLinkStatus.Dying)
|
||||
{
|
||||
ap.LogDebug("SendDeathLink(): Not sending a deathlink because we're in the process of dying to one");
|
||||
return;
|
||||
}
|
||||
|
||||
if (service == null)
|
||||
{
|
||||
ap.LogDebug("SendDeathLink(): Not sending a deathlink because not enabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((DateTime.UtcNow - lastDamageTime).TotalSeconds > 5)
|
||||
{
|
||||
ap.LogWarn("Last damage was a long time ago, resetting damage type to zero.");
|
||||
// Damage source was more than 5 seconds ago, so ignore damage type
|
||||
lastDamageType = 0;
|
||||
}
|
||||
|
||||
string message = DeathLinkMessages.GetDeathMessage(lastDamageType, ArchipelagoMod.Instance.session.Players.ActivePlayer.Alias);
|
||||
ap.LogDebug(
|
||||
$"SendDeathLink(): Sending deathlink. \"{message}\"");
|
||||
service.SendDeathLink(new DeathLink(ArchipelagoMod.Instance.session.Players.ActivePlayer.Alias, message));
|
||||
}
|
||||
|
||||
private void OnDeathLinkReceived(DeathLink deathLink)
|
||||
{
|
||||
ArchipelagoMod ap = ArchipelagoMod.Instance;
|
||||
ap.LogDebug($"OnDeathLinkReceived(): Receiving deathlink. Status={status}.");
|
||||
|
||||
if (status == DeathLinkStatus.None)
|
||||
{
|
||||
status = DeathLinkStatus.Pending;
|
||||
|
||||
string cause = deathLink.Cause;
|
||||
if (cause == null || cause == "")
|
||||
{
|
||||
cause = $"{deathLink.Source} died.";
|
||||
}
|
||||
|
||||
MenuChanger.ThreadSupport.BeginInvoke(() =>
|
||||
{
|
||||
new ItemChanger.UIDefs.MsgUIDef()
|
||||
{
|
||||
name = new BoxedString(cause),
|
||||
sprite = new ArchipelagoSprite { key = "DeathLinkIcon" }
|
||||
}.SendMessage(MessageType.Corner, null);
|
||||
});
|
||||
|
||||
lastDamageType = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
ap.LogDebug("Skipping this deathlink as one is currently in progress");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using ItemChanger;
|
||||
using ItemChanger.Items;
|
||||
using ItemChanger.Modules;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.Modules;
|
||||
|
||||
public class DupeHandlingModule : Module
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AbstractItem.ModifyRedundantItemGlobal += ModifyRedundantItem;
|
||||
}
|
||||
|
||||
public override void Unload()
|
||||
{
|
||||
AbstractItem.ModifyRedundantItemGlobal -= ModifyRedundantItem;
|
||||
}
|
||||
|
||||
private void ModifyRedundantItem(GiveEventArgs args)
|
||||
{
|
||||
args.Item = new SpawnLumafliesItem
|
||||
{
|
||||
name = $"Lumafly_Escape-{args.Orig.name}",
|
||||
UIDef = DupeUIDef.Of(args.Orig.UIDef)
|
||||
};
|
||||
}
|
||||
}
|
||||
159
Mod/Archipelago.HollowKnight/IC/Modules/GiftingModule.cs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
using Archipelago.Gifting.Net.Service;
|
||||
using Archipelago.Gifting.Net.Traits;
|
||||
using Archipelago.Gifting.Net.Versioning.Gifts.Current;
|
||||
using Archipelago.MultiClient.Net;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Modules;
|
||||
using ItemChanger.Tags;
|
||||
using MenuChanger;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.Modules;
|
||||
|
||||
public class GiftingModule : Module
|
||||
{
|
||||
private static readonly string[] AcceptedTraits = [GiftFlag.Mana, GiftFlag.Life, "Artifact"];
|
||||
|
||||
private ArchipelagoSession session => ArchipelagoMod.Instance.session;
|
||||
private GiftingService giftingService;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
if (ArchipelagoMod.Instance.GS.EnableGifting)
|
||||
{
|
||||
giftingService = new GiftingService(session);
|
||||
On.GameManager.FinishedEnteringScene += BeginGifting;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unload()
|
||||
{
|
||||
if (giftingService != null)
|
||||
{
|
||||
giftingService.CloseGiftBox();
|
||||
giftingService.OnNewGift -= ReceiveOrRefundGift;
|
||||
giftingService = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void BeginGifting(On.GameManager.orig_FinishedEnteringScene orig, GameManager self)
|
||||
{
|
||||
orig(self);
|
||||
On.GameManager.FinishedEnteringScene -= BeginGifting;
|
||||
giftingService.OnNewGift += ReceiveOrRefundGift;
|
||||
Dictionary<string, Gift> pendingGifts = await giftingService.CheckGiftBoxAsync();
|
||||
foreach (Gift gift in pendingGifts.Values)
|
||||
{
|
||||
ReceiveOrRefundGift(gift);
|
||||
}
|
||||
giftingService.OpenGiftBox(false, AcceptedTraits);
|
||||
}
|
||||
|
||||
private void ReceiveOrRefundGift(Gift gift)
|
||||
{
|
||||
giftingService.RemoveGiftFromGiftBox(gift.ID);
|
||||
GiftTrait bestTrait = PickBestMatchingTrait(gift);
|
||||
if (bestTrait != null)
|
||||
{
|
||||
string itemName;
|
||||
if (bestTrait.Trait == GiftFlag.Mana)
|
||||
{
|
||||
// soul refill based on quality. Average is 90 (large totems), below is 54, above is 200, scaling linearly
|
||||
if (bestTrait.Quality >= 2.2)
|
||||
{
|
||||
itemName = ItemNames.Soul_Totem_Path_of_Pain;
|
||||
}
|
||||
else if (bestTrait.Quality <= 0.6)
|
||||
{
|
||||
itemName = ItemNames.Soul_Totem_B;
|
||||
}
|
||||
else
|
||||
{
|
||||
itemName = ItemNames.Soul_Totem_A;
|
||||
}
|
||||
}
|
||||
else if (bestTrait.Trait == GiftFlag.Life)
|
||||
{
|
||||
// blue hearts based on quality. Average is 2 blue masks, scaling linearly from that
|
||||
// todo - do we want XL/XS lifeblood for more interesting variance?
|
||||
if (bestTrait.Quality >= 1.5)
|
||||
{
|
||||
itemName = ItemNames.Lifeblood_Cocoon_Large;
|
||||
}
|
||||
else
|
||||
{
|
||||
itemName = ItemNames.Lifeblood_Cocoon_Small;
|
||||
}
|
||||
}
|
||||
else if (bestTrait.Trait == "Artifact")
|
||||
{
|
||||
// relic based on quality (average case is Hallownest seal, scales roughly linearly from that)
|
||||
if (bestTrait.Quality <= 0.4)
|
||||
{
|
||||
itemName = ItemNames.Wanderers_Journal;
|
||||
}
|
||||
else if (bestTrait.Quality >= 2.7)
|
||||
{
|
||||
itemName = ItemNames.Arcane_Egg;
|
||||
}
|
||||
else if (bestTrait.Quality >= 1.8)
|
||||
{
|
||||
itemName = ItemNames.Kings_Idol;
|
||||
}
|
||||
else
|
||||
{
|
||||
itemName = ItemNames.Hallownest_Seal;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// safety net in case we update acceptedtraits
|
||||
ArchipelagoMod.Instance.LogWarn($"Got an unexpected trait {bestTrait} for gift {gift}");
|
||||
giftingService.RefundGift(gift);
|
||||
return;
|
||||
}
|
||||
|
||||
string sender = session.Players.GetPlayerName(gift.SenderSlot);
|
||||
DispatchItem(itemName, gift.Amount, sender);
|
||||
}
|
||||
else
|
||||
{
|
||||
giftingService.RefundGift(gift);
|
||||
}
|
||||
}
|
||||
|
||||
private GiftTrait PickBestMatchingTrait(Gift gift)
|
||||
{
|
||||
GiftTrait best = null;
|
||||
foreach (GiftTrait trait in gift.Traits)
|
||||
{
|
||||
if (AcceptedTraits.Contains(trait.Trait))
|
||||
{
|
||||
if (best == null || trait.Quality > best.Quality)
|
||||
{
|
||||
best = trait;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private void DispatchItem(string itemName, int amount, string sender)
|
||||
{
|
||||
ThreadSupport.BeginInvoke(() =>
|
||||
{
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
AbstractItem item = Finder.GetItem(itemName);
|
||||
InteropTag recentItemsTag = item.AddTag<InteropTag>();
|
||||
recentItemsTag.Message = "RecentItems";
|
||||
recentItemsTag.Properties["DisplaySource"] = sender;
|
||||
|
||||
item.Load();
|
||||
item.Give(null, ItemNetworkingModule.RemoteGiveInfo);
|
||||
item.Unload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
59
Mod/Archipelago.HollowKnight/IC/Modules/GoalModule.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
using Archipelago.MultiClient.Net;
|
||||
using Archipelago.MultiClient.Net.Enums;
|
||||
using Archipelago.MultiClient.Net.Exceptions;
|
||||
using Archipelago.MultiClient.Net.Packets;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Modules;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.Modules
|
||||
{
|
||||
public class GoalModule : Module
|
||||
{
|
||||
private ArchipelagoSession session => ArchipelagoMod.Instance.session;
|
||||
|
||||
private Goal goal;
|
||||
|
||||
public bool queuedGoal = false;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
goal = Goal.GetGoal(ArchipelagoMod.Instance.SlotData.Options.Goal);
|
||||
goal.Select();
|
||||
Events.OnEnterGame += OnEnterGame;
|
||||
}
|
||||
|
||||
public override void Unload()
|
||||
{
|
||||
Events.OnEnterGame -= OnEnterGame;
|
||||
goal.Deselect();
|
||||
goal = null;
|
||||
}
|
||||
|
||||
public async Task DeclareVictoryAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await session.Socket.SendPacketAsync(new StatusUpdatePacket()
|
||||
{
|
||||
Status = ArchipelagoClientState.ClientGoal
|
||||
}).TimeoutAfter(1000);
|
||||
queuedGoal = false;
|
||||
}
|
||||
catch (Exception ex) when (ex is TimeoutException or ArchipelagoSocketClosedException)
|
||||
{
|
||||
ItemChangerMod.Modules.Get<ItemNetworkingModule>().ReportDisconnect();
|
||||
queuedGoal = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnEnterGame()
|
||||
{
|
||||
if (queuedGoal)
|
||||
{
|
||||
await DeclareVictoryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
372
Mod/Archipelago.HollowKnight/IC/Modules/ItemNetworkingModule.cs
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
using Archipelago.HollowKnight.IC.Tags;
|
||||
using Archipelago.MultiClient.Net;
|
||||
using Archipelago.MultiClient.Net.Exceptions;
|
||||
using Archipelago.MultiClient.Net.Models;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Internal;
|
||||
using ItemChanger.Modules;
|
||||
using ItemChanger.Tags;
|
||||
using ItemChanger.Items;
|
||||
using MenuChanger;
|
||||
using Modding;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.Modules
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the sending and receiving of items from the server
|
||||
/// </summary>
|
||||
public class ItemNetworkingModule : Module
|
||||
{
|
||||
/// <summary>
|
||||
/// A preset GiveInfo structure that avoids creating geo and places messages in the corner.
|
||||
/// </summary>
|
||||
public static GiveInfo RemoteGiveInfo = new()
|
||||
{
|
||||
FlingType = FlingType.DirectDeposit,
|
||||
Callback = null,
|
||||
Container = Container.Unknown,
|
||||
MessageType = MessageType.Corner
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A preset GiveInfo structure that avoids creating geo and outputs no messages, e.g. for Start Items.
|
||||
/// </summary>
|
||||
public static GiveInfo SilentGiveInfo = new()
|
||||
{
|
||||
FlingType = FlingType.DirectDeposit,
|
||||
Callback = null,
|
||||
Container = Container.Unknown,
|
||||
MessageType = MessageType.None
|
||||
};
|
||||
|
||||
private ArchipelagoSession session => ArchipelagoMod.Instance.session;
|
||||
|
||||
private bool networkErrored;
|
||||
private bool readyToSendReceiveChecks;
|
||||
|
||||
[JsonProperty]
|
||||
private List<long> deferredLocationChecks = [];
|
||||
[JsonProperty]
|
||||
private bool hasEverRecievedStartingGeo = false;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
networkErrored = false;
|
||||
readyToSendReceiveChecks = false;
|
||||
ModHooks.HeroUpdateHook += PollForItems;
|
||||
On.GameManager.FinishedEnteringScene += DoInitialSyncAndStartSendReceive;
|
||||
}
|
||||
|
||||
public override void Unload()
|
||||
{
|
||||
// DoInitialSyncAndStartSendReceive unsubscribes itself
|
||||
ModHooks.HeroUpdateHook -= PollForItems;
|
||||
session.Locations.CheckedLocationsUpdated -= OnLocationChecksUpdated;
|
||||
}
|
||||
|
||||
public async Task SendLocationsAsync(params long[] locationIds)
|
||||
{
|
||||
if (!readyToSendReceiveChecks)
|
||||
{
|
||||
deferredLocationChecks.AddRange(locationIds);
|
||||
return;
|
||||
}
|
||||
|
||||
if (networkErrored)
|
||||
{
|
||||
deferredLocationChecks.AddRange(locationIds);
|
||||
ReportDisconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Run(() => session.Locations.CompleteLocationChecks(locationIds)).TimeoutAfter(1000);
|
||||
}
|
||||
catch (Exception ex) when (ex is TimeoutException or ArchipelagoSocketClosedException)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogWarn("SendLocationsAsync disconnected");
|
||||
deferredLocationChecks.AddRange(locationIds);
|
||||
ReportDisconnect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogError("Unexpected error in SendLocationsAsync");
|
||||
ArchipelagoMod.Instance.LogError(ex);
|
||||
deferredLocationChecks.AddRange(locationIds);
|
||||
ReportDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkLocationAsChecked(long locationId, bool silentGive)
|
||||
{
|
||||
// Called when marking a location as checked remotely (i.e. through ReceiveItem, etc.)
|
||||
// This also grants items at said locations.
|
||||
bool hadNewlyObtainedItems = false;
|
||||
bool hadUnobtainedItems = false;
|
||||
|
||||
ArchipelagoMod.Instance.LogDebug($"Marking location {locationId} as checked.");
|
||||
if (!ArchipelagoPlacementTag.PlacementsByLocationId.TryGetValue(locationId, out AbstractPlacement pmt))
|
||||
{
|
||||
ArchipelagoMod.Instance.LogDebug($"Could not find a placement for location {locationId}");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (AbstractItem item in pmt.Items)
|
||||
{
|
||||
if (!item.GetTag(out ArchipelagoItemTag tag))
|
||||
{
|
||||
hadUnobtainedItems = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.WasEverObtained())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tag.Location != locationId)
|
||||
{
|
||||
hadUnobtainedItems = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
hadNewlyObtainedItems = true;
|
||||
pmt.AddVisitFlag(VisitState.ObtainedAnyItem);
|
||||
|
||||
GiveInfo giveInfo = silentGive ? SilentGiveInfo : RemoteGiveInfo;
|
||||
item.Give(pmt, giveInfo.Clone());
|
||||
}
|
||||
|
||||
if (hadNewlyObtainedItems && !hadUnobtainedItems)
|
||||
{
|
||||
pmt.AddVisitFlag(VisitState.Opened | VisitState.Dropped | VisitState.Accepted |
|
||||
VisitState.ObtainedAnyItem);
|
||||
}
|
||||
}
|
||||
|
||||
private async void DoInitialSyncAndStartSendReceive(On.GameManager.orig_FinishedEnteringScene orig, GameManager self)
|
||||
{
|
||||
orig(self);
|
||||
if (!readyToSendReceiveChecks)
|
||||
{
|
||||
On.GameManager.FinishedEnteringScene -= DoInitialSyncAndStartSendReceive;
|
||||
if (!hasEverRecievedStartingGeo)
|
||||
{
|
||||
HeroController.instance.AddGeo(ArchipelagoMod.Instance.SlotData.Options.StartingGeo);
|
||||
hasEverRecievedStartingGeo = true;
|
||||
}
|
||||
readyToSendReceiveChecks = true;
|
||||
await Synchronize();
|
||||
session.Locations.CheckedLocationsUpdated += OnLocationChecksUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLocationChecksUpdated(ReadOnlyCollection<long> newCheckedLocations)
|
||||
{
|
||||
foreach (long location in newCheckedLocations)
|
||||
{
|
||||
ThreadSupport.BeginInvoke(() => MarkLocationAsChecked(location, false));
|
||||
}
|
||||
}
|
||||
|
||||
private void PollForItems()
|
||||
{
|
||||
if (!readyToSendReceiveChecks || !session.Items.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReceiveNextItem(false);
|
||||
}
|
||||
|
||||
private async Task Synchronize()
|
||||
{
|
||||
// receive from the server any items that are pending
|
||||
while (ReceiveNextItem(true)) { }
|
||||
// ensure any already-checked locations (co-op, restarting save) are marked cleared
|
||||
foreach (long location in session.Locations.AllLocationsChecked)
|
||||
{
|
||||
MarkLocationAsChecked(location, true);
|
||||
}
|
||||
// send out any pending items that didn't get to the network from the previous session
|
||||
long[] pendingLocations = deferredLocationChecks.ToArray();
|
||||
deferredLocationChecks.Clear();
|
||||
await SendLocationsAsync(pendingLocations);
|
||||
}
|
||||
|
||||
private bool ReceiveNextItem(bool silentGive)
|
||||
{
|
||||
if (!session.Items.Any())
|
||||
{
|
||||
return false; // No items are waiting.
|
||||
}
|
||||
|
||||
ItemInfo itemInfo = session.Items.DequeueItem(); // Read the next item
|
||||
|
||||
try
|
||||
{
|
||||
ReceiveItem(itemInfo, silentGive);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogError($"Unexpected exception during receive for item {JsonConvert.SerializeObject(itemInfo.ToSerializable())}: {ex}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ReceiveItem(ItemInfo itemInfo, bool silentGive)
|
||||
{
|
||||
string name = itemInfo.ItemName;
|
||||
ArchipelagoMod.Instance.LogDebug($"Receiving item {itemInfo.ItemId} with name {name}. " +
|
||||
$"Slot is {itemInfo.Player}. Location is {itemInfo.LocationId} with name {itemInfo.LocationName}");
|
||||
|
||||
|
||||
|
||||
ArchipelagoRemoteItemCounterModule remoteTracker = ItemChangerMod.Modules.GetOrAdd<ArchipelagoRemoteItemCounterModule>();
|
||||
bool shouldReceive = remoteTracker.ShouldReceiveServerItem(itemInfo);
|
||||
remoteTracker.IncrementServerCountForItem(itemInfo);
|
||||
ArchipelagoMod.Instance.LogDebug($"shouldRecieve has value {shouldReceive}");
|
||||
ArchipelagoMod.Instance.LogDebug($"ActivePlayer.Slot is {ArchipelagoMod.Instance.session.Players.ActivePlayer.Slot}");
|
||||
|
||||
// If we have received a spell, we should unlock a dreamer as well with that setting enabled
|
||||
if(ArchipelagoMod.Instance.SlotData.Options.AltBlackEgg) {
|
||||
string[] spellNames = ["Vengeful_Spirit", "Shade_Soul", "Desolate_Dive", "Descending_Dark", "Howling_Wraiths", "Abyss_Shriek"];
|
||||
if (spellNames.Any(name.Contains)) {
|
||||
if (shouldReceive || (itemInfo.Player == ArchipelagoMod.Instance.session.Players.ActivePlayer.Slot && itemInfo.LocationId > 0))
|
||||
{
|
||||
DreamerItem dreamItem = new DreamerItem();
|
||||
GiveInfo dreamInfo = silentGive ? SilentGiveInfo : RemoteGiveInfo;
|
||||
dreamItem.GiveImmediate(dreamInfo.Clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have received enough mask shards to gain a Mask, we should unlock a White Fragment as well with that setting enabled
|
||||
if(ArchipelagoMod.Instance.SlotData.Options.AltRadiance) {
|
||||
string[] shardNames = ["Mask_Shard", "Double_Mask_Shard", "Full_Mask"];
|
||||
if (shardNames.Any(name.Contains)) {
|
||||
if (shouldReceive || (itemInfo.Player == ArchipelagoMod.Instance.session.Players.ActivePlayer.Slot && itemInfo.LocationId > 0))
|
||||
{
|
||||
// Getting our current player instance
|
||||
PlayerData pd = PlayerData.instance;
|
||||
|
||||
// Counting our current heart pieces
|
||||
int heartPieces = pd.GetInt(nameof(PlayerData.heartPieces));
|
||||
ArchipelagoMod.Instance.LogDebug($"Current heartPieces value: {heartPieces}");
|
||||
int heartIndex = Array.IndexOf(shardNames, name);
|
||||
if ((itemInfo.Player == ArchipelagoMod.Instance.session.Players.ActivePlayer.Slot && itemInfo.LocationId > 0))
|
||||
{
|
||||
heartPieces = (heartPieces + 3) % 4; //local mask shards get applied before this function so we account for that here
|
||||
}
|
||||
switch (heartIndex)
|
||||
{
|
||||
case 0:
|
||||
heartPieces += 1;
|
||||
break;
|
||||
case 1:
|
||||
heartPieces += 2;
|
||||
break;
|
||||
case 2:
|
||||
heartPieces += 4;
|
||||
break;
|
||||
}
|
||||
ArchipelagoMod.Instance.LogDebug($"Post addition heartPieces value: {heartPieces}");
|
||||
// If we have over 3 heart pieces we get a White Fragment
|
||||
if ( heartPieces >= 4) {
|
||||
|
||||
WhiteFragmentItem fragmentItem = new WhiteFragmentItem();
|
||||
int royalCharmLevel = pd.GetInt(nameof(PlayerData.royalCharmState));
|
||||
fragmentItem.royalCharmLevel = 2;
|
||||
|
||||
switch (royalCharmLevel)
|
||||
{
|
||||
case 1:
|
||||
case 2:
|
||||
fragmentItem.royalCharmLevel = 3;
|
||||
break;
|
||||
case 3:
|
||||
fragmentItem.royalCharmLevel = 4;
|
||||
break;
|
||||
}
|
||||
|
||||
GiveInfo fragmentInfo = silentGive ? SilentGiveInfo : RemoteGiveInfo;
|
||||
fragmentItem.GiveImmediate(fragmentInfo.Clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (itemInfo.Player == ArchipelagoMod.Instance.session.Players.ActivePlayer.Slot && itemInfo.LocationId > 0)
|
||||
{
|
||||
MarkLocationAsChecked(itemInfo.LocationId, silentGive);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldReceive)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogDebug($"Fast-forwarding past already received item {name} from {itemInfo.Player} at location {itemInfo.LocationDisplayName} ({itemInfo.LocationId})");
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're still here, this is an item from someone else. We'll make up our own dummy placement and grant the item.
|
||||
AbstractItem item = Finder.GetItem(name);
|
||||
if (item == null)
|
||||
{
|
||||
ArchipelagoMod.Instance.LogWarn($"Could not find an item named '{name}'. " +
|
||||
$"This means that item {itemInfo.ItemId} was not received.");
|
||||
return;
|
||||
}
|
||||
ArchipelagoRemoteItemTag remoteItemTag = new(itemInfo);
|
||||
item.AddTag(remoteItemTag);
|
||||
|
||||
string sender;
|
||||
if (itemInfo.LocationId == -1)
|
||||
{
|
||||
sender = "Cheat Console";
|
||||
}
|
||||
else if (itemInfo.LocationId == -2)
|
||||
{
|
||||
sender = "Start";
|
||||
}
|
||||
else if (itemInfo.Player == 0)
|
||||
{
|
||||
sender = "Archipelago";
|
||||
}
|
||||
else
|
||||
{
|
||||
sender = session.Players.GetPlayerName(itemInfo.Player);
|
||||
}
|
||||
InteropTag recentItemsTag = item.AddTag<InteropTag>();
|
||||
recentItemsTag.Message = "RecentItems";
|
||||
recentItemsTag.Properties["DisplaySource"] = sender;
|
||||
|
||||
RemotePlacement pmt = RemotePlacement.GetOrAddSingleton();
|
||||
item.Load();
|
||||
pmt.Add(item);
|
||||
|
||||
GiveInfo giveInfo = silentGive ? SilentGiveInfo : RemoteGiveInfo;
|
||||
item.Give(pmt, giveInfo.Clone());
|
||||
}
|
||||
|
||||
public void ReportDisconnect()
|
||||
{
|
||||
networkErrored = true;
|
||||
MessageController.Enqueue(
|
||||
null,
|
||||
"Error: Lost connection to Archipelago server"
|
||||
);
|
||||
MessageController.Enqueue(
|
||||
null,
|
||||
"Reload your save to attempt to reconnect."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
using ItemChanger.Modules;
|
||||
using Modding;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.Modules
|
||||
{
|
||||
public class RepositionShadeModule : Module
|
||||
{
|
||||
private static readonly Dictionary<string, (float x, float y)> ShadeSpawnPositionFixes = new()
|
||||
{
|
||||
{ "Abyss_08", (90.0f, 90.0f) }, // Lifeblood Core room. Even outside of deathlink, shades spawn out of bounds.
|
||||
{ "Room_Colosseum_Spectate", (124.0f, 10.0f) }, // Shade spawns inside inaccessible arena
|
||||
{ "Resting_Grounds_09", (7.4f, 10.0f) }, // Shade spawns underground.
|
||||
{ "Runes1_18", (11.5f, 23.0f) }, // Shade potentially spawns on the wrong side of an inaccessible gate.
|
||||
};
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
ModHooks.AfterPlayerDeadHook += FixUnreachableShadePosition;
|
||||
}
|
||||
|
||||
public override void Unload()
|
||||
{
|
||||
ModHooks.AfterPlayerDeadHook -= FixUnreachableShadePosition;
|
||||
}
|
||||
private void FixUnreachableShadePosition()
|
||||
{
|
||||
// Fixes up some bad shade placements by vanilla HK.
|
||||
PlayerData pd = PlayerData.instance;
|
||||
if (ShadeSpawnPositionFixes.TryGetValue(pd.shadeScene, out (float x, float y) position))
|
||||
{
|
||||
pd.shadePositionX = position.x;
|
||||
pd.shadePositionY = position.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Mod/Archipelago.HollowKnight/IC/PlacementUtils.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using ItemChanger;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC
|
||||
{
|
||||
internal static class PlacementUtils
|
||||
{
|
||||
internal static IEnumerable<long> GetLocationIDs(AbstractPlacement pmt)
|
||||
{
|
||||
ArchipelagoItemTag tag;
|
||||
foreach (AbstractItem item in pmt.Items)
|
||||
{
|
||||
tag = item.GetTag<ArchipelagoItemTag>();
|
||||
if (tag != null)
|
||||
{
|
||||
yield return tag.Location;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
178
Mod/Archipelago.HollowKnight/IC/RM/HelperPlatformBuilder.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
using Archipelago.HollowKnight.SlotDataModel;
|
||||
using ItemChanger;
|
||||
using System.Collections.Generic;
|
||||
using SD = ItemChanger.Util.SceneDataUtil;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.RM
|
||||
{
|
||||
public static class HelperPlatformBuilder
|
||||
{
|
||||
public static IBool hasWalljump = new PDBool(nameof(PlayerData.hasWalljump));
|
||||
public static IBool hasWalljumpLeft = new PDBool(nameof(ItemChanger.Modules.SplitClaw.hasWalljumpLeft));
|
||||
public static IBool hasWalljumpRight = new PDBool(nameof(ItemChanger.Modules.SplitClaw.hasWalljumpRight));
|
||||
public static IBool hasDoubleJump = new PDBool(nameof(PlayerData.hasDoubleJump));
|
||||
|
||||
public static IBool lacksLeftClaw = new Negation(new Disjunction(hasWalljump, hasWalljumpLeft));
|
||||
public static IBool lacksLeftVertical = new Negation(new Disjunction(hasWalljump, hasDoubleJump, hasWalljumpLeft));
|
||||
public static IBool lacksRightClaw = new Negation(new Disjunction(hasWalljump, hasWalljumpRight));
|
||||
public static IBool lacksRightVertical = new Negation(new Disjunction(hasWalljump, hasDoubleJump, hasWalljumpRight));
|
||||
public static IBool lacksAnyClaw = new Negation(new Disjunction(hasWalljump, hasWalljumpLeft, hasWalljumpRight));
|
||||
public static IBool lacksAnyVertical = new Negation(new Disjunction(hasWalljump, hasDoubleJump, hasWalljumpLeft, hasWalljumpRight));
|
||||
|
||||
public static void AddStartLocationRequiredPlatforms(SlotOptions options)
|
||||
{
|
||||
string startLocationName = options.StartLocationName ?? StartLocationNames.Kings_Pass;
|
||||
|
||||
switch (startLocationName)
|
||||
{
|
||||
// Platforms to allow escaping the Hive start regardless of difficulty or initial items
|
||||
case StartLocationNames.Hive:
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform { SceneName = SceneNames.Hive_03, X = 58.5f, Y = 134f, Test = lacksRightClaw });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform { SceneName = SceneNames.Hive_03, X = 58.5f, Y = 138.5f, Test = lacksAnyVertical });
|
||||
break;
|
||||
|
||||
// Drop the vine platforms and add small platforms for jumping up.
|
||||
case StartLocationNames.Far_Greenpath:
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform { SceneName = SceneNames.Fungus1_13, X = 45f, Y = 16.5f, Test = lacksLeftClaw });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform { SceneName = SceneNames.Fungus1_13, X = 64f, Y = 16.5f, Test = lacksRightClaw });
|
||||
SD.Save(SceneNames.Fungus1_13, "Vine Platform (1)");
|
||||
SD.Save(SceneNames.Fungus1_13, "Vine Platform (2)");
|
||||
break;
|
||||
|
||||
// With the Lower Greenpath start, getting to the rest of Greenpath requires
|
||||
// cutting the vine to the right of the vessel fragment.
|
||||
case StartLocationNames.Lower_Greenpath:
|
||||
if (options.RandomizeNail)
|
||||
{
|
||||
SD.Save(SceneNames.Fungus1_13, "Vine Platform");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddConveniencePlatforms(SlotOptions options)
|
||||
{
|
||||
// FUTURE: when we support room rando, this should be updated based on transition placements.
|
||||
HashSet<string> targetNames = new();
|
||||
string startLocationName = options.StartLocationName ?? StartLocationNames.Kings_Pass;
|
||||
|
||||
if (!options.ExtraPlatforms)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Platforms to climb out from basin wanderer's journal
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_02, X = 128.3f, Y = 7f, Test = lacksLeftClaw });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_02, X = 128.3f, Y = 11f, Test = lacksLeftClaw });
|
||||
|
||||
// Platforms to climb up to tram in basin from left with no items
|
||||
if (!targetNames.Contains($"{SceneNames.Abyss_03}[bot1]"))
|
||||
{
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_03, X = 34f, Y = 7f, Test = lacksRightVertical });
|
||||
}
|
||||
|
||||
// Platform to climb out of Abyss with only wings
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_06_Core, X = 88.6f, Y = 263f, Test = lacksLeftClaw });
|
||||
|
||||
// Platforms to climb back up from pale ore with no items
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 164.7f, Y = 30f, Test = lacksRightVertical });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 99.5f, Y = 12.5f, Test = lacksRightVertical });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 117.7f, Y = 18.8f, Test = lacksRightClaw });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 114.3f, Y = 23f, Test = lacksAnyVertical });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 117.7f, Y = 7f, Test = lacksAnyClaw });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_17, X = 117.7f, Y = 10.8f, Test = lacksAnyClaw });
|
||||
|
||||
// Platforms to remove softlock with wings at simple key in basin
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Abyss_20, X = 26.5f, Y = 13f, Test = lacksAnyClaw });
|
||||
|
||||
// Platform for returning to Gorb landing
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Cliffs_02, X = 32.3f, Y = 27.7f, Test = lacksAnyVertical });
|
||||
|
||||
// Platform to return from Deepnest mimic grub room
|
||||
if (!targetNames.Contains($"{SceneNames.Deepnest_01b}[right2]")
|
||||
&& !targetNames.Contains($"{SceneNames.Deepnest_02}[left1]")
|
||||
&& !targetNames.Contains($"{SceneNames.Deepnest_02}[right1]"))
|
||||
{
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Deepnest_01b, X = 48.3f, Y = 40f, Test = lacksAnyVertical });
|
||||
}
|
||||
|
||||
// Platforms to return from the Deepnest_02 geo rocks without vertical
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Deepnest_02, X = 26f, Y = 12f, Test = lacksAnyClaw });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Deepnest_02, X = 26f, Y = 16f, Test = lacksAnyClaw });
|
||||
|
||||
// Platform to escape the Deepnest mimic room when mimics may not be present
|
||||
if (options.RandomizeMimics)
|
||||
{
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Deepnest_36, X = 26f, Y = 11f, Test = lacksLeftVertical });
|
||||
}
|
||||
|
||||
// Platforms to climb back up from Mantis Lords with only wings
|
||||
if (!targetNames.Contains($"{SceneNames.Fungus2_15}[left1]")
|
||||
&& !targetNames.Contains($"{SceneNames.Fungus2_25}[top1]")
|
||||
&& !targetNames.Contains($"{SceneNames.Fungus2_25}[top2]"))
|
||||
{
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Fungus2_15, X = 48f + 2 * i, Y = 15f + 10 * i, Test = lacksRightClaw });
|
||||
}
|
||||
}
|
||||
|
||||
// Platforms to prevent softlock on lever on the way to love key.
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Fungus3_05, X = 65.7f, Y = 11f + 4.5f * i, Test = lacksRightClaw });
|
||||
}
|
||||
|
||||
if (startLocationName != StartLocationNames.Hive)
|
||||
{
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Hive_03, X = 58.5f, Y = 134f, Test = lacksAnyVertical });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Hive_03, X = 58.5f, Y = 138.5f, Test = lacksAnyVertical });
|
||||
}
|
||||
|
||||
// Move the load in colo downward to prevent bench soft lock
|
||||
if (!targetNames.Contains($"{SceneNames.Room_Colosseum_02}[top2]")
|
||||
&& !targetNames.Contains($"{SceneNames.Room_Colosseum_Spectate}[right1]"))
|
||||
{
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Room_Colosseum_02, X = 43.5f, Y = 45f, Test = lacksAnyClaw });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Room_Colosseum_02, X = 43.5f, Y = 49.5f, Test = lacksAnyClaw });
|
||||
}
|
||||
|
||||
// Platform to escape from the geo rock above Lemm
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Ruins1_05c, X = 26.6f, Y = 73.2f, Test = lacksAnyVertical });
|
||||
|
||||
// Platforms to climb back up to King's Pass with no items
|
||||
if (!targetNames.Contains($"{SceneNames.Town}[right1]") && startLocationName == StartLocationNames.Kings_Pass)
|
||||
{
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Town, X = 20f - 2 * (i % 2), Y = 5f * i + 15f, Test = lacksLeftClaw });
|
||||
}
|
||||
}
|
||||
|
||||
// Platforms to prevent itemless softlock when checking left waterways
|
||||
if (!targetNames.Contains($"{SceneNames.Waterways_04}[left1]")
|
||||
&& !targetNames.Contains($"{SceneNames.Waterways_04}[left2]")
|
||||
&& !targetNames.Contains($"{SceneNames.Waterways_04b}[left1]")
|
||||
&& !targetNames.Contains($"{SceneNames.Waterways_09}[left1]")
|
||||
&& startLocationName != StartLocationNames.West_Waterways)
|
||||
{
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Waterways_04, X = 148f, Y = 23.1f, Test = lacksAnyVertical });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform() { SceneName = SceneNames.Waterways_04, X = 139f, Y = 32f, Test = lacksAnyVertical });
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform()
|
||||
{
|
||||
SceneName = SceneNames.Waterways_04,
|
||||
X = 107f,
|
||||
Y = 10f,
|
||||
Test = options.RandomizeSwim ? new PDBool("canSwim") : null
|
||||
});
|
||||
ItemChangerMod.AddDeployer(new SmallPlatform()
|
||||
{
|
||||
SceneName = SceneNames.Waterways_04,
|
||||
X = 107f,
|
||||
Y = 15f,
|
||||
Test = options.RandomizeSwim ? new PDBool("canSwim") : null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
504
Mod/Archipelago.HollowKnight/IC/RM/LICENSE
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random
|
||||
Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
19
Mod/Archipelago.HollowKnight/IC/RM/NOTICE.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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).
|
||||
|
||||
## Changes made
|
||||
|
||||
### HelperPlatformBuilder.cs
|
||||
|
||||
* Moved and renamed from RandomizerMod/IC/PlatformList.cs
|
||||
* Added start-dependent helper platforms from RandomizerMod/IC/Export.cs's ExportStart method
|
||||
* Changed namespaces and imports according to new file structure
|
||||
* Changed arguments of `GetPlatformList` to use Archipelago's settings objects rather than RandomizerMod's
|
||||
|
||||
|
||||
### StartLocationSceneEditsModule
|
||||
|
||||
* Moved and renamed from RandomizerMod/IC/RandomizerModule.cs
|
||||
* Changed namespaces and imports according to new file structure
|
||||
* Scoped to only handle scene edits to prevent starting softlocks
|
||||
* Adapted to use Archipelago's settings rather than RandomizerMod's
|
||||
* Formatted to match this project's formatting style
|
||||
54
Mod/Archipelago.HollowKnight/IC/RM/StartDef.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using GlobalEnums;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.RM;
|
||||
public record StartDef
|
||||
{
|
||||
public static Dictionary<string, StartDef> Lookup
|
||||
{
|
||||
get
|
||||
{
|
||||
if (field != null)
|
||||
{
|
||||
return field;
|
||||
}
|
||||
JsonSerializer ser = new()
|
||||
{
|
||||
TypeNameHandling = TypeNameHandling.Auto,
|
||||
Converters =
|
||||
{
|
||||
new StringEnumConverter()
|
||||
}
|
||||
};
|
||||
using StreamReader r = new(typeof(StartDef).Assembly.GetManifestResourceStream("Archipelago.HollowKnight.Resources.Data.starts.json"));
|
||||
using JsonTextReader reader = new(r);
|
||||
return field = ser.Deserialize<Dictionary<string, StartDef>>(reader);
|
||||
}
|
||||
}
|
||||
|
||||
public string Name { get; init; }
|
||||
public string SceneName { get; init; }
|
||||
public float X { get; init; }
|
||||
public float Y { get; init; }
|
||||
public MapZone Zone { get; init; }
|
||||
/// <summary>
|
||||
/// Granted transition in logic
|
||||
/// </summary>
|
||||
public string Transition { get; init; }
|
||||
|
||||
public ItemChanger.StartDef ToItemChangerStartDef()
|
||||
{
|
||||
return new ItemChanger.StartDef
|
||||
{
|
||||
SceneName = SceneName,
|
||||
X = X,
|
||||
Y = Y,
|
||||
MapZone = (int)Zone,
|
||||
RespawnFacingRight = true,
|
||||
SpecialEffects = ItemChanger.SpecialStartEffects.Default | ItemChanger.SpecialStartEffects.SlowSoulRefill,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
using Archipelago.HollowKnight.SlotDataModel;
|
||||
using ItemChanger;
|
||||
using ItemChanger.Extensions;
|
||||
using ItemChanger.Modules;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UObject = UnityEngine.Object;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.RM;
|
||||
public class StartLocationSceneEditsModule : Module
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
ToggleSceneHooks(true);
|
||||
}
|
||||
|
||||
public override void Unload()
|
||||
{
|
||||
ToggleSceneHooks(false);
|
||||
}
|
||||
|
||||
private static void ToggleSceneHooks(bool toggle)
|
||||
{
|
||||
SlotOptions options = ArchipelagoMod.Instance.SlotData.Options;
|
||||
string startLocation = options.StartLocationName ?? StartLocationNames.Kings_Pass;
|
||||
|
||||
switch (startLocation)
|
||||
{
|
||||
case "Ancestral Mound":
|
||||
if (options.RandomizeNail)
|
||||
{
|
||||
if (toggle)
|
||||
{
|
||||
Events.AddSceneChangeEdit(SceneNames.Crossroads_ShamanTemple, DestroyPlanksForAncestralMoundStart);
|
||||
}
|
||||
else
|
||||
{
|
||||
Events.RemoveSceneChangeEdit(SceneNames.Crossroads_ShamanTemple, DestroyPlanksForAncestralMoundStart);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "Fungal Core":
|
||||
if (toggle)
|
||||
{
|
||||
Events.AddSceneChangeEdit(SceneNames.Fungus2_30, CreateBounceShroomsForFungalCoreStart);
|
||||
}
|
||||
else
|
||||
{
|
||||
Events.RemoveSceneChangeEdit(SceneNames.Fungus2_30, CreateBounceShroomsForFungalCoreStart);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "West Crossroads":
|
||||
if (toggle)
|
||||
{
|
||||
Events.AddSceneChangeEdit(SceneNames.Crossroads_36, MoveShadeMarkerForWestCrossroadsStart);
|
||||
}
|
||||
else
|
||||
{
|
||||
Events.RemoveSceneChangeEdit(SceneNames.Crossroads_36, MoveShadeMarkerForWestCrossroadsStart);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Destroy planks in cursed nail mode because we can't slash them
|
||||
private static void DestroyPlanksForAncestralMoundStart(Scene to)
|
||||
{
|
||||
foreach ((_, GameObject go) in to.Traverse())
|
||||
{
|
||||
if (go.name.StartsWith("Plank"))
|
||||
{
|
||||
UObject.Destroy(go);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateBounceShroomsForFungalCoreStart(Scene to)
|
||||
{
|
||||
GameObject bounceShroom = to.FindGameObjectByName("Bounce Shroom C");
|
||||
|
||||
GameObject s0 = UObject.Instantiate(bounceShroom);
|
||||
s0.transform.SetPosition3D(12.5f, 26f, 0f);
|
||||
s0.SetActive(true);
|
||||
|
||||
GameObject s1 = UObject.Instantiate(bounceShroom);
|
||||
s1.transform.SetPosition3D(12.5f, 54f, 0f);
|
||||
s1.SetActive(true);
|
||||
|
||||
GameObject s2 = UObject.Instantiate(bounceShroom);
|
||||
s2.transform.SetPosition3D(21.7f, 133f, 0f);
|
||||
s2.SetActive(true);
|
||||
}
|
||||
|
||||
private static void MoveShadeMarkerForWestCrossroadsStart(Scene to)
|
||||
{
|
||||
GameObject marker = to.FindGameObject("_Props/Hollow_Shade Marker 1");
|
||||
marker.transform.position = new(46.2f, 28f);
|
||||
}
|
||||
}
|
||||
47
Mod/Archipelago.HollowKnight/IC/RemotePlacement.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
using ItemChanger;
|
||||
using ItemChanger.Extensions;
|
||||
using ItemChanger.Internal;
|
||||
using ItemChanger.Tags;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC
|
||||
{
|
||||
public class RemotePlacement : AbstractPlacement
|
||||
{
|
||||
public const string SINGLETON_NAME = "Remote_Items";
|
||||
|
||||
[JsonConstructor]
|
||||
private RemotePlacement(string Name) : base(SINGLETON_NAME) { }
|
||||
|
||||
public static RemotePlacement GetOrAddSingleton()
|
||||
{
|
||||
if (!Ref.Settings.Placements.TryGetValue(SINGLETON_NAME, out AbstractPlacement pmt))
|
||||
{
|
||||
pmt = new RemotePlacement(SINGLETON_NAME);
|
||||
CompletionWeightTag remoteCompletionWeightTag = pmt.AddTag<CompletionWeightTag>();
|
||||
remoteCompletionWeightTag.Weight = 0;
|
||||
InteropTag pinTag = new()
|
||||
{
|
||||
Message = "RandoSupplementalMetadata",
|
||||
Properties = new()
|
||||
{
|
||||
["DoNotMakePin"] = true,
|
||||
}
|
||||
};
|
||||
pmt.AddTag(pinTag);
|
||||
ItemChangerMod.AddPlacements(pmt.Yield());
|
||||
}
|
||||
return (RemotePlacement)pmt;
|
||||
}
|
||||
|
||||
protected override void OnLoad()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void OnUnload()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
using Archipelago.HollowKnight.IC.Modules;
|
||||
using Archipelago.MultiClient.Net.Models;
|
||||
using ItemChanger;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
namespace Archipelago.HollowKnight.IC.Tags;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tag attached to items sent from other players
|
||||
/// </summary>
|
||||
public class ArchipelagoRemoteItemTag : Tag
|
||||
{
|
||||
/// <summary>
|
||||
/// The slot ID of the sending player
|
||||
/// </summary>
|
||||
public int Sender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The location ID in the sender's world
|
||||
/// </summary>
|
||||
public long LocationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The item ID
|
||||
/// </summary>
|
||||
public long ItemId { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
private ArchipelagoRemoteItemTag() { }
|
||||
|
||||
public ArchipelagoRemoteItemTag(ItemInfo itemInfo)
|
||||
{
|
||||
if (itemInfo is ScoutedItemInfo)
|
||||
{
|
||||
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));
|
||||
}
|
||||
ArchipelagoMod.Instance.LogDebug($"Created remote tag for {itemInfo.ItemName} from {itemInfo.Player} at {itemInfo.LocationDisplayName}");
|
||||
Sender = itemInfo.Player;
|
||||
LocationId = itemInfo.LocationId;
|
||||
ItemId = itemInfo.ItemId;
|
||||
}
|
||||
|
||||
public override void Load(object parent)
|
||||
{
|
||||
base.Load(parent);
|
||||
ArchipelagoRemoteItemCounterModule module = ItemChangerMod.Modules.GetOrAdd<ArchipelagoRemoteItemCounterModule>();
|
||||
module.IncrementSavedCountForItem(Sender, LocationId, ItemId);
|
||||
}
|
||||
}
|
||||
19
Mod/Archipelago.HollowKnight/LoginValidationException.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
|
||||
namespace Archipelago.HollowKnight
|
||||
{
|
||||
internal class LoginValidationException : Exception
|
||||
{
|
||||
public LoginValidationException()
|
||||
{
|
||||
}
|
||||
|
||||
public LoginValidationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public LoginValidationException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
using MenuChanger;
|
||||
using MenuChanger.Extensions;
|
||||
using MenuChanger.MenuElements;
|
||||
using MenuChanger.MenuPanels;
|
||||
using Modding;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Archipelago.HollowKnight.MC
|
||||
{
|
||||
internal class ArchipelagoModeMenuConstructor : ModeMenuConstructor
|
||||
{
|
||||
private MenuPage modeConfigPage;
|
||||
|
||||
private readonly static Type _settingsType = typeof(ConnectionDetails);
|
||||
private readonly static Font _perpetua = CanvasUtil.GetFont("Perpetua");
|
||||
|
||||
public override void OnEnterMainMenu(MenuPage modeMenu)
|
||||
{
|
||||
modeConfigPage = new MenuPage("Archipelago Settings", modeMenu);
|
||||
ConnectionDetails settings = ArchipelagoMod.Instance.GS.MenuConnectionDetails;
|
||||
|
||||
EntryField<string> urlField = CreateUrlField(modeConfigPage, settings);
|
||||
NumericEntryField<int> portField = CreatePortField(modeConfigPage, settings);
|
||||
EntryField<string> nameField = CreateSlotNameField(modeConfigPage, settings);
|
||||
EntryField<string> passwordField = CreatePasswordField(modeConfigPage, settings);
|
||||
|
||||
MenuLabel errorLabel = new(modeConfigPage, "");
|
||||
BigButton startButton = new(modeConfigPage, "Start", "May stall after clicking");
|
||||
|
||||
startButton.AddSetResumeKeyEvent("Archipelago");
|
||||
startButton.OnClick += () => StartOrResumeGame(true, errorLabel);
|
||||
|
||||
modeConfigPage.AfterHide += () => errorLabel.Text.text = "";
|
||||
|
||||
IMenuElement[] elements =
|
||||
[
|
||||
urlField,
|
||||
portField,
|
||||
nameField,
|
||||
passwordField,
|
||||
startButton,
|
||||
errorLabel
|
||||
];
|
||||
VerticalItemPanel vip = new(modeConfigPage, SpaceParameters.TOP_CENTER_UNDER_TITLE, 100, false, elements);
|
||||
modeConfigPage.AddToNavigationControl(vip);
|
||||
|
||||
AttachResumePage();
|
||||
}
|
||||
|
||||
private void AttachResumePage()
|
||||
{
|
||||
MenuPage resumePage = new("Archipelago Resume");
|
||||
|
||||
MenuLabel slotName = new(resumePage, "");
|
||||
|
||||
EntryField<string> urlField = CreateUrlField(resumePage, null);
|
||||
NumericEntryField<int> portField = CreatePortField(resumePage, null);
|
||||
EntryField<string> passwordField = CreatePasswordField(resumePage, null);
|
||||
|
||||
SmallButton resumeButton = new(resumePage, "Resume");
|
||||
MenuLabel errorLabel = new(resumePage, "");
|
||||
|
||||
void RebindSettings()
|
||||
{
|
||||
ConnectionDetails settings = ArchipelagoMod.Instance.LS.ConnectionDetails;
|
||||
if (settings != null)
|
||||
{
|
||||
slotName.Text.text = $"Slot Name: {settings.SlotName}";
|
||||
urlField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerUrl)));
|
||||
portField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerPort)));
|
||||
passwordField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerPassword)));
|
||||
}
|
||||
else
|
||||
{
|
||||
slotName.Text.text = "Incompatible save file";
|
||||
errorLabel.Text.text = "To resume, recreate your save or downgrade to an older client version.";
|
||||
}
|
||||
}
|
||||
|
||||
resumeButton.OnClick += () => StartOrResumeGame(false, errorLabel);
|
||||
|
||||
resumePage.BeforeShow += RebindSettings;
|
||||
resumePage.AfterHide += () => errorLabel.Text.text = "";
|
||||
|
||||
IMenuElement[] elements =
|
||||
[
|
||||
slotName,
|
||||
urlField,
|
||||
portField,
|
||||
passwordField,
|
||||
resumeButton,
|
||||
errorLabel
|
||||
];
|
||||
|
||||
VerticalItemPanel vip = new(resumePage, SpaceParameters.TOP_CENTER_UNDER_TITLE, 100, true, elements);
|
||||
resumePage.AddToNavigationControl(vip);
|
||||
|
||||
ResumeMenu.AddResumePage("Archipelago", resumePage);
|
||||
}
|
||||
|
||||
private static EntryField<string> CreateUrlField(MenuPage apPage, ConnectionDetails settings)
|
||||
{
|
||||
EntryField<string> urlField = new(apPage, "Server URL: ");
|
||||
urlField.InputField.characterLimit = 500;
|
||||
RectTransform urlRect = urlField.InputField.gameObject.transform.Find("Text").GetComponent<RectTransform>();
|
||||
urlRect.sizeDelta = new Vector2(1500f, 63.2f);
|
||||
urlField.InputField.textComponent.font = _perpetua;
|
||||
if (settings != null)
|
||||
{
|
||||
urlField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerUrl)));
|
||||
}
|
||||
return urlField;
|
||||
}
|
||||
|
||||
private static NumericEntryField<int> CreatePortField(MenuPage apPage, ConnectionDetails settings)
|
||||
{
|
||||
NumericEntryField<int> portField = new(apPage, "Server Port: ");
|
||||
portField.SetClamp(0, 65535);
|
||||
portField.InputField.textComponent.font = _perpetua;
|
||||
if (settings != null)
|
||||
{
|
||||
portField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerPort)));
|
||||
}
|
||||
return portField;
|
||||
}
|
||||
|
||||
private static EntryField<string> CreateSlotNameField(MenuPage apPage, ConnectionDetails settings)
|
||||
{
|
||||
EntryField<string> nameField = new(apPage, "Slot Name: ");
|
||||
nameField.InputField.characterLimit = 500;
|
||||
nameField.InputField.textComponent.font = _perpetua;
|
||||
RectTransform nameRect = nameField.InputField.gameObject.transform.Find("Text").GetComponent<RectTransform>();
|
||||
nameRect.sizeDelta = new Vector2(1500f, 63.2f);
|
||||
if (settings != null)
|
||||
{
|
||||
nameField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.SlotName)));
|
||||
}
|
||||
return nameField;
|
||||
}
|
||||
|
||||
private static EntryField<string> CreatePasswordField(MenuPage apPage, ConnectionDetails settings)
|
||||
{
|
||||
EntryField<string> passwordField = new(apPage, "Password: ");
|
||||
passwordField.InputField.characterLimit = 500;
|
||||
passwordField.InputField.textComponent.font = _perpetua;
|
||||
RectTransform passwordRect = passwordField.InputField.gameObject.transform.Find("Text").GetComponent<RectTransform>();
|
||||
passwordRect.sizeDelta = new Vector2(1500f, 63.2f);
|
||||
if (settings != null)
|
||||
{
|
||||
passwordField.Bind(settings, _settingsType.GetProperty(nameof(ConnectionDetails.ServerPassword)));
|
||||
}
|
||||
return passwordField;
|
||||
}
|
||||
|
||||
private static void StartOrResumeGame(bool newGame, MenuLabel errorLabel)
|
||||
{
|
||||
ArchipelagoMod.Instance.ArchipelagoEnabled = true;
|
||||
|
||||
// Cloning some settings onto others depending on what is taking precedence.
|
||||
// If it's a save slot we're resuming (newGame == false) then we want the slot settings to overwrite the global ones.
|
||||
if (newGame)
|
||||
{
|
||||
ArchipelagoMod.Instance.LS = new APLocalSettings()
|
||||
{
|
||||
ConnectionDetails = ArchipelagoMod.Instance.GS.MenuConnectionDetails with { },
|
||||
};
|
||||
}
|
||||
else if (ArchipelagoMod.Instance.LS.ConnectionDetails != null)
|
||||
{
|
||||
ArchipelagoMod.Instance.GS.MenuConnectionDetails = ArchipelagoMod.Instance.LS.ConnectionDetails with { };
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ArchipelagoMod.Instance.StartOrResumeGame(newGame);
|
||||
MenuChangerMod.HideAllMenuPages();
|
||||
if (newGame)
|
||||
{
|
||||
UIManager.instance.StartNewGame();
|
||||
}
|
||||
else
|
||||
{
|
||||
UIManager.instance.ContinueGame();
|
||||
GameManager.instance.ContinueGame();
|
||||
}
|
||||
}
|
||||
catch (LoginValidationException ex)
|
||||
{
|
||||
ArchipelagoMod.Instance.DisconnectArchipelago();
|
||||
errorLabel.Text.text = ex.Message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorLabel.Text.text = "An unknown error occurred when attempting to connect.";
|
||||
ArchipelagoMod.Instance.LogError(ex);
|
||||
ArchipelagoMod.Instance.DisconnectArchipelago();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnExitMainMenu()
|
||||
{
|
||||
modeConfigPage = null;
|
||||
}
|
||||
|
||||
public override bool TryGetModeButton(MenuPage modeMenu, out BigButton button)
|
||||
{
|
||||
button = new BigButton(modeMenu, ArchipelagoMod.Instance.spriteManager.GetSprite("IconColorBig"), "Archipelago");
|
||||
button.AddHideAndShowEvent(modeMenu, modeConfigPage);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Mod/Archipelago.HollowKnight/ModDependencies.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ItemChanger
|
||||
MenuChanger
|
||||
Benchwarp
|
||||
267
Mod/Archipelago.HollowKnight/Resources/Data/starts.json
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
{
|
||||
"King's Pass": {
|
||||
"name": "King's Pass",
|
||||
"sceneName": "Tutorial_01",
|
||||
"x": 35.5,
|
||||
"y": 11.4,
|
||||
"zone": "KINGS_PASS",
|
||||
"transition": "Tutorial_01[right1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"Stag Nest": {
|
||||
"name": "Stag Nest",
|
||||
"sceneName": "Cliffs_03",
|
||||
"x": 85.8,
|
||||
"y": 46.4,
|
||||
"zone": "CLIFFS",
|
||||
"transition": "Cliffs_03[right1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"West Crossroads": {
|
||||
"name": "West Crossroads",
|
||||
"sceneName": "Crossroads_36",
|
||||
"x": 40.2,
|
||||
"y": 22.0,
|
||||
"zone": "CROSSROADS",
|
||||
"transition": "Crossroads_36[right1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"East Crossroads": {
|
||||
"name": "East Crossroads",
|
||||
"sceneName": "Crossroads_03",
|
||||
"x": 14.0,
|
||||
"y": 68.0,
|
||||
"zone": "CROSSROADS",
|
||||
"transition": "Crossroads_03[top1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"Ancestral Mound": {
|
||||
"name": "Ancestral Mound",
|
||||
"sceneName": "Crossroads_ShamanTemple",
|
||||
"x": 37.7,
|
||||
"y": 46.5,
|
||||
"zone": "SHAMAN_TEMPLE",
|
||||
"transition": "Crossroads_ShamanTemple[left1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"West Fog Canyon": {
|
||||
"name": "West Fog Canyon",
|
||||
"sceneName": "Fungus3_30",
|
||||
"x": 35.1,
|
||||
"y": 16.4,
|
||||
"zone": "FOG_CANYON",
|
||||
"transition": "Fungus3_30[bot1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"East Fog Canyon": {
|
||||
"name": "East Fog Canyon",
|
||||
"sceneName": "Fungus3_25",
|
||||
"x": 77.5,
|
||||
"y": 23.7,
|
||||
"zone": "FOG_CANYON",
|
||||
"transition": "Fungus3_25[right1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"Queen's Station": {
|
||||
"name": "Queen's Station",
|
||||
"sceneName": "Fungus2_01",
|
||||
"x": 24.0,
|
||||
"y": 37.4,
|
||||
"zone": "QUEENS_STATION",
|
||||
"transition": "Fungus2_01[left1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"Fungal Wastes": {
|
||||
"name": "Fungal Wastes",
|
||||
"sceneName": "Fungus2_28",
|
||||
"x": 59.6,
|
||||
"y": 3.4,
|
||||
"zone": "WASTES",
|
||||
"transition": "Fungus2_28[left1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"Greenpath": {
|
||||
"name": "Greenpath",
|
||||
"sceneName": "Fungus1_32",
|
||||
"x": 3.8,
|
||||
"y": 27.4,
|
||||
"zone": "GREEN_PATH",
|
||||
"transition": "Fungus1_32[left1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"Lower Greenpath": {
|
||||
"name": "Lower Greenpath",
|
||||
"sceneName": "Fungus1_13",
|
||||
"x": 126.2,
|
||||
"y": 37.4,
|
||||
"zone": "GREEN_PATH",
|
||||
"transition": "Fungus1_13[right1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"West Blue Lake": {
|
||||
"name": "West Blue Lake",
|
||||
"sceneName": "Crossroads_50",
|
||||
"x": 21.2,
|
||||
"y": 44.4,
|
||||
"zone": "CROSSROADS",
|
||||
"transition": "Crossroads_50[left1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"East Blue Lake": {
|
||||
"name": "East Blue Lake",
|
||||
"sceneName": "Crossroads_50",
|
||||
"x": 225.2,
|
||||
"y": 25.4,
|
||||
"zone": "CROSSROADS",
|
||||
"transition": "Crossroads_50[right1]",
|
||||
"logic": "(ITEMRANDO | MAPAREARANDO) + (ENEMYPOGOS | ELEVATOR) | FULLAREARANDO | ROOMRANDO"
|
||||
// first branch: ensure upper Resting Grounds or King's Station is accessible
|
||||
},
|
||||
"City Storerooms": {
|
||||
"name": "City Storerooms",
|
||||
"sceneName": "Ruins1_17",
|
||||
"x": 61.6,
|
||||
"y": 3.4,
|
||||
"zone": "CITY",
|
||||
"transition": "Ruins1_17[right1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"King's Station": {
|
||||
"name": "King's Station",
|
||||
"sceneName": "Ruins2_10b",
|
||||
"x": 20.9,
|
||||
"y": 136.3,
|
||||
"zone": "CITY",
|
||||
"transition": "Ruins2_10b[right1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"Outside Colosseum": {
|
||||
"name": "Outside Colosseum",
|
||||
"sceneName": "Deepnest_East_09",
|
||||
"x": 159.9,
|
||||
"y": 12.4,
|
||||
"zone": "COLOSSEUM",
|
||||
"transition": "Deepnest_East_09[right1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"Crystallized Mound": {
|
||||
"name": "Crystallized Mound",
|
||||
"sceneName": "Mines_35",
|
||||
"x": 3.2,
|
||||
"y": 48.4,
|
||||
"zone": "MINES",
|
||||
"transition": "Mines_35[left1]",
|
||||
"logic": "ANY"
|
||||
},
|
||||
"Mantis Village": {
|
||||
"name": "Mantis Village",
|
||||
"sceneName": "Fungus2_14",
|
||||
"x": 117.8,
|
||||
"y": 15.4,
|
||||
"zone": "WASTES",
|
||||
"transition": "Fungus2_14[right1]",
|
||||
"logic": "(ITEMRANDO | MAPAREARANDO | FULLAREARANDO) + ENEMYPOGOS | ROOMRANDO | VERTICAL"
|
||||
// first branch: ensure Queen's Station (or on full area, Fungus2_12[left1]) is reachable
|
||||
},
|
||||
"Kingdom's Edge": {
|
||||
"name": "Kingdom's Edge",
|
||||
"sceneName": "Deepnest_East_15",
|
||||
"x": 26.5,
|
||||
"y": 4.4,
|
||||
"zone": "OUTSKIRTS",
|
||||
"transition": "Deepnest_East_15[left1]",
|
||||
"logic": "(ITEMRANDO + ENEMYPOGOS + SWIM) | MAPAREARANDO | FULLAREARANDO | ROOMRANDO"
|
||||
// first branch: ensure King's Station Stag is reachable
|
||||
},
|
||||
"Hallownest's Crown": {
|
||||
"name": "Hallownest's Crown",
|
||||
"sceneName": "Mines_34",
|
||||
"x": 128.3,
|
||||
"y": 46.4,
|
||||
"zone": "MINES",
|
||||
"transition": "Mines_34[bot1]",
|
||||
"logic": "(ITEMRANDO | MAPAREARANDO) + DARKROOMS | FULLAREARANDO | ROOMRANDO"
|
||||
},
|
||||
"West Waterways": {
|
||||
"name": "West Waterways",
|
||||
"sceneName": "Waterways_09",
|
||||
"x": 34.7,
|
||||
"y": 30.4,
|
||||
"zone": "WATERWAYS",
|
||||
"transition": "Waterways_09[left1]",
|
||||
"logic": "(ITEMRANDO + ENEMYPOGOS + SHADESKIPS + 2MASKS) | MAPAREARANDO | FULLAREARANDO | ROOMRANDO"
|
||||
},
|
||||
"Queen's Gardens": {
|
||||
"name": "Queen's Gardens",
|
||||
"sceneName": "Fungus3_13",
|
||||
"x": 25.3,
|
||||
"y": 63.4,
|
||||
"zone": "ROYAL_GARDENS",
|
||||
"transition": "Fungus3_13[left1]",
|
||||
"logic": "(ITEMRANDO | MAPAREARANDO | FULLAREARANDO) + ENEMYPOGOS + DANGEROUSSKIPS | ROOMRANDO"
|
||||
// skip logic is minimum to ensure Hallownest_Seal-Queen's_Gardens is reachable
|
||||
},
|
||||
"Distant Village": {
|
||||
"name": "Distant Village",
|
||||
"sceneName": "Room_spider_small",
|
||||
"x": 23.1,
|
||||
"y": 13.4,
|
||||
"zone": "DEEPNEST",
|
||||
"transition": "Room_spider_small[left1]",
|
||||
"logic": "FULLAREARANDO | ROOMRANDO"
|
||||
},
|
||||
"Far Greenpath": {
|
||||
"name": "Far Greenpath",
|
||||
"sceneName": "Fungus1_13",
|
||||
"x": 34.9,
|
||||
"y": 23.4,
|
||||
"zone": "GREEN_PATH",
|
||||
"transition": "Fungus1_13[left1]",
|
||||
"logic": "MAPAREARANDO | FULLAREARANDO | ROOMRANDO"
|
||||
},
|
||||
"Hive": {
|
||||
"name": "Hive",
|
||||
"sceneName": "Hive_03",
|
||||
"x": 47.2,
|
||||
"y": 142.7,
|
||||
"zone": "HIVE",
|
||||
"transition": "Hive_03[right1]",
|
||||
"logic": "ROOMRANDO"
|
||||
},
|
||||
"Royal Waterways": {
|
||||
"name": "Royal Waterways",
|
||||
"sceneName": "Waterways_03",
|
||||
"x": 93.6,
|
||||
"y": 4.4,
|
||||
"zone": "WATERWAYS",
|
||||
"transition": "Waterways_03[left1]",
|
||||
"logic": "ROOMRANDO"
|
||||
},
|
||||
"City of Tears": {
|
||||
"name": "City of Tears",
|
||||
"sceneName": "Ruins1_27",
|
||||
"x": 29.6,
|
||||
"y": 6.4,
|
||||
"zone": "CITY",
|
||||
"transition": "Ruins1_27[left1]",
|
||||
"logic": "ROOMRANDO"
|
||||
},
|
||||
"Abyss": {
|
||||
"name": "Abyss",
|
||||
"sceneName": "Abyss_06_Core",
|
||||
"x": 42.0,
|
||||
"y": 5.4,
|
||||
"zone": "ABYSS",
|
||||
"transition": "Abyss_06_Core[right2]",
|
||||
"logic": "ROOMRANDO"
|
||||
},
|
||||
"Fungal Core": {
|
||||
"name": "Fungal Core",
|
||||
"sceneName": "Fungus2_30",
|
||||
"x": 64.8,
|
||||
"y": 21.4,
|
||||
"zone": "WASTES",
|
||||
"transition": "Fungus2_30[top1]",
|
||||
"logic": "ROOMRANDO"
|
||||
}
|
||||
}
|
||||
BIN
Mod/Archipelago.HollowKnight/Resources/DeathLinkIcon.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
Mod/Archipelago.HollowKnight/Resources/GrubHappyv2.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
Mod/Archipelago.HollowKnight/Resources/IconBlue.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
Mod/Archipelago.HollowKnight/Resources/IconBlueBig.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
Mod/Archipelago.HollowKnight/Resources/IconBlueSmall.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
Mod/Archipelago.HollowKnight/Resources/IconColor.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
Mod/Archipelago.HollowKnight/Resources/IconColorBig.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
Mod/Archipelago.HollowKnight/Resources/IconColorSmall.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
Mod/Archipelago.HollowKnight/Resources/Pins/pinAP.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Mod/Archipelago.HollowKnight/Resources/Pins/pinAPProgression.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
Mod/Archipelago.HollowKnight/Resources/Pins/pinAPUseful.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
23
Mod/Archipelago.HollowKnight/Settings.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
namespace Archipelago.HollowKnight
|
||||
{
|
||||
public record ConnectionDetails
|
||||
{
|
||||
public string ServerUrl { get; set; } = "archipelago.gg";
|
||||
public int ServerPort { get; set; } = 38281;
|
||||
public string SlotName { get; set; }
|
||||
public string ServerPassword { get; set; }
|
||||
}
|
||||
|
||||
public record APGlobalSettings
|
||||
{
|
||||
public ConnectionDetails MenuConnectionDetails { get; set; } = new();
|
||||
public bool EnableGifting { get; set; } = true;
|
||||
}
|
||||
|
||||
public record APLocalSettings
|
||||
{
|
||||
public ConnectionDetails ConnectionDetails { get; set; }
|
||||
public string RoomSeed { get; set; }
|
||||
public long Seed { get; set; }
|
||||
}
|
||||
}
|
||||
26
Mod/Archipelago.HollowKnight/SlotDataModel/SlotData.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Archipelago.HollowKnight.SlotDataModel
|
||||
{
|
||||
public class SlotData
|
||||
{
|
||||
[JsonProperty("seed")]
|
||||
public int Seed { get; set; }
|
||||
|
||||
[JsonProperty("options")]
|
||||
public SlotOptions Options { get; set; }
|
||||
|
||||
[JsonProperty("location_costs")]
|
||||
public Dictionary<string, Dictionary<string, int>> LocationCosts { get; set; }
|
||||
|
||||
[JsonProperty("notch_costs")]
|
||||
public List<int> NotchCosts { get; set; }
|
||||
|
||||
[JsonProperty("grub_count")]
|
||||
public int? GrubsRequired { get; set; }
|
||||
|
||||
[JsonProperty("is_race")]
|
||||
public bool DisableLocalSpoilerLogs { get; set; }
|
||||
}
|
||||
}
|
||||
143
Mod/Archipelago.HollowKnight/SlotDataModel/SlotOptions.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
namespace Archipelago.HollowKnight.SlotDataModel
|
||||
{
|
||||
public class SlotOptions
|
||||
{
|
||||
public bool RandomizeDreamers { get; set; }
|
||||
|
||||
public bool RandomizeSkills { get; set; }
|
||||
|
||||
public bool RandomizeFocus { get; set; }
|
||||
|
||||
public bool RandomizeSwim { get; set; }
|
||||
|
||||
public bool RandomizeCharms { get; set; }
|
||||
|
||||
public bool RandomizeKeys { get; set; }
|
||||
|
||||
public bool RandomizeMaskShards { get; set; }
|
||||
|
||||
public bool RandomizeVesselFragments { get; set; }
|
||||
|
||||
public bool RandomizeCharmNotches { get; set; }
|
||||
|
||||
public bool RandomizePaleOre { get; set; }
|
||||
|
||||
public bool RandomizeGeoChests { get; set; }
|
||||
|
||||
public bool RandomizeJunkPitChests { get; set; }
|
||||
|
||||
public bool RandomizeRancidEggs { get; set; }
|
||||
|
||||
public bool RandomizeRelics { get; set; }
|
||||
|
||||
public bool RandomizeWhisperingRoots { get; set; }
|
||||
|
||||
public bool RandomizeBossEssence { get; set; }
|
||||
|
||||
public bool RandomizeGrubs { get; set; }
|
||||
|
||||
public bool RandomizeMimics { get; set; }
|
||||
|
||||
public bool RandomizeMaps { get; set; }
|
||||
|
||||
public bool RandomizeStags { get; set; }
|
||||
|
||||
public bool RandomizeLifebloodCocoons { get; set; }
|
||||
|
||||
public bool RandomizeGrimmkinFlames { get; set; }
|
||||
|
||||
public bool RandomizeJournalEntries { get; set; }
|
||||
|
||||
public bool RandomizeNail { get; set; }
|
||||
|
||||
public bool RandomizeGeoRocks { get; set; }
|
||||
|
||||
public bool RandomizeBossGeo { get; set; }
|
||||
|
||||
public bool RandomizeSoulTotems { get; set; }
|
||||
|
||||
public bool RandomizeLoreTablets { get; set; }
|
||||
|
||||
public bool AltBlackEgg { get; set; }
|
||||
|
||||
public bool AltRadiance { get; set; }
|
||||
|
||||
public bool PreciseMovement { get; set; }
|
||||
|
||||
public bool ProficientCombat { get; set; }
|
||||
|
||||
public bool BackgroundObjectPogos { get; set; }
|
||||
|
||||
public bool EnemyPogos { get; set; }
|
||||
|
||||
public bool ObscureSkips { get; set; }
|
||||
|
||||
public bool ShadeSkips { get; set; }
|
||||
|
||||
public bool InfectionSkips { get; set; }
|
||||
|
||||
public bool FireballSkips { get; set; }
|
||||
|
||||
public bool SpikeTunnels { get; set; }
|
||||
|
||||
public bool AcidSkips { get; set; }
|
||||
|
||||
public bool DamageBoosts { get; set; }
|
||||
|
||||
public bool DangerousSkips { get; set; }
|
||||
|
||||
public bool DarkRooms { get; set; }
|
||||
|
||||
public bool ComplexSkips { get; set; }
|
||||
|
||||
public bool DifficultSkips { get; set; }
|
||||
|
||||
public bool Slopeballs { get; set; }
|
||||
|
||||
public bool ShriekPogos { get; set; }
|
||||
|
||||
public bool RemoveSpellUpgrades { get; set; }
|
||||
|
||||
public bool RandomizeElevatorPass { get; set; }
|
||||
|
||||
public string StartLocationName { get; set; }
|
||||
|
||||
public int MinimumGrubPrice { get; set; }
|
||||
|
||||
public int MaximumGrubPrice { get; set; }
|
||||
|
||||
public int MinimumEssencePrice { get; set; }
|
||||
|
||||
public int MaximumEssencePrice { get; set; }
|
||||
|
||||
public int MinimumEggPrice { get; set; }
|
||||
|
||||
public int MaximumEggPrice { get; set; }
|
||||
|
||||
public int RandomCharmCosts { get; set; }
|
||||
|
||||
public int EggShopSlots { get; set; }
|
||||
|
||||
public GoalsLookup Goal { get; set; }
|
||||
|
||||
public bool DeathLink { get; set; }
|
||||
|
||||
public DeathLinkShadeHandling DeathLinkShade { get; set; }
|
||||
|
||||
public bool DeathLinkBreaksFragileCharms { get; set; }
|
||||
|
||||
public WhitePalaceOption WhitePalace { get; set; }
|
||||
|
||||
public bool ExtraPlatforms { get; set; }
|
||||
|
||||
public int StartingGeo { get; set; }
|
||||
|
||||
public bool SplitMantisClaw { get; set; }
|
||||
|
||||
public bool SplitMothwingCloak { get; set; }
|
||||
|
||||
public bool SplitCrystalHeart { get; set; }
|
||||
|
||||
public bool AddUnshuffledLocations { get; set; }
|
||||
}
|
||||
}
|
||||
83
Mod/Archipelago.HollowKnight/TimeoutExtensions.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Archipelago.HollowKnight
|
||||
{
|
||||
// implementation from https://devblogs.microsoft.com/pfxteam/crafting-a-task-timeoutafter-method/
|
||||
internal static class TimeoutExtensions
|
||||
{
|
||||
private struct VoidTypeStruct { }
|
||||
|
||||
private static void MarshalTaskResults<TResult>(Task source, TaskCompletionSource<TResult> proxy)
|
||||
{
|
||||
switch (source.Status)
|
||||
{
|
||||
case TaskStatus.Faulted:
|
||||
proxy.TrySetException(source.Exception);
|
||||
break;
|
||||
case TaskStatus.Canceled:
|
||||
proxy.TrySetCanceled();
|
||||
break;
|
||||
case TaskStatus.RanToCompletion:
|
||||
Task<TResult> castedSource = source as Task<TResult>;
|
||||
proxy.TrySetResult(
|
||||
castedSource == null ? default : // source is a Task
|
||||
castedSource.Result); // source is a Task<TResult>
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static Task TimeoutAfter(this Task task, int millisecondsTimeout)
|
||||
{
|
||||
// Short-circuit #1: infinite timeout or task already completed
|
||||
if (task.IsCompleted || (millisecondsTimeout == Timeout.Infinite))
|
||||
{
|
||||
// Either the task has already completed or timeout will never occur.
|
||||
// No proxy necessary.
|
||||
return task;
|
||||
}
|
||||
|
||||
// tcs.Task will be returned as a proxy to the caller
|
||||
TaskCompletionSource<VoidTypeStruct> tcs = new();
|
||||
|
||||
// Short-circuit #2: zero timeout
|
||||
if (millisecondsTimeout == 0)
|
||||
{
|
||||
// We've already timed out.
|
||||
tcs.SetException(new TimeoutException());
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
// Set up a timer to complete after the specified timeout period
|
||||
Timer timer = new Timer(state =>
|
||||
{
|
||||
// Recover your state information
|
||||
var myTcs = (TaskCompletionSource<VoidTypeStruct>)state;
|
||||
|
||||
// Fault our proxy with a TimeoutException
|
||||
myTcs.TrySetException(new TimeoutException());
|
||||
}, tcs, millisecondsTimeout, Timeout.Infinite);
|
||||
|
||||
// Wire up the logic for what happens when source task completes
|
||||
task.ContinueWith((antecedent, state) =>
|
||||
{
|
||||
// Recover our state data
|
||||
var tuple =
|
||||
(Tuple<Timer, TaskCompletionSource<VoidTypeStruct>>)state;
|
||||
|
||||
// Cancel the Timer
|
||||
tuple.Item1.Dispose();
|
||||
|
||||
// Marshal results to proxy
|
||||
MarshalTaskResults(antecedent, tuple.Item2);
|
||||
},
|
||||
Tuple.Create(timer, tcs),
|
||||
CancellationToken.None,
|
||||
TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Default);
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Mod/LICENSE.txt
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Hussein Farran
|
||||
Copyright (c) 2022 Daniel Grace
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
41
Mod/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Archipelago.HollowKnight
|
||||
|
||||
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).
|
||||
|
||||
## Installing Archipelago.HollowKnight
|
||||
### Installing with Lumafly
|
||||
1. [Download Lumafly](https://themulhima.github.io/Lumafly?download).
|
||||
2. Place Lumafly in a folder other than your Downloads folder and run it
|
||||
* If it does not detect your HK install directory, lead Lumafly to the correct directory.
|
||||
* Also, don’t pirate the game. >:(
|
||||
3. Install and enable Archipelago.
|
||||
* There are several mods that are needed to for Archipelago to run. They are installed automatically.
|
||||
* Archipelago Map Mod is an in-game tracker for Archipelago. It is optional and can also be installed from Lumafly.
|
||||
4. Start the game and ensure **Archipelago** appears in the top left corner of the main menu.
|
||||
|
||||
## Joining an Archipelago Session
|
||||
1. Start the game after installing all necessary mods.
|
||||
2. Create a **new save game.**
|
||||
3. Select the **Archipelago** game mode from the mode selection screen.
|
||||
4. Enter in the correct settings for your Archipelago server.
|
||||
5. Hit **Start** to begin the game. The game will stall for a few seconds while it does all item placements.
|
||||
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.
|
||||
|
||||
# Contributing
|
||||
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.
|
||||
|
||||
Contribution guidelines:
|
||||
* All issues should be labeled appropriately.
|
||||
* All in-progress issues should have someone assigned to them.
|
||||
* Pull Requests must have at least (and preferably exactly) one linked issue which they close out.
|
||||
* Please use feature branches, especially if working in this repository (not a fork).
|
||||
* Please match the style of the surrounding code. In particular:
|
||||
* Don't use `var`.
|
||||
* Use shorthand constructor syntax in declarations, and only in declarations (for example, `ArchipelagoRandomizer randomizer = new(slotData);`).
|
||||
* Always enclose the body of control flow statements (`if`, `foreach`, etc.) in braces, even for single-line bodies.
|
||||
|
||||
## Development Setup
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||