Compare commits

..

3 commits

Author SHA1 Message Date
Sakimori ef0f523f7a
adjusted mod readme to reflect new providence 2025-10-12 19:17:37 -04:00
Sakimori b2e79243d1
added Mod files 2025-10-12 19:15:58 -04:00
Sakimori d7d69af56c
initial attributes and ignore files 2025-10-12 19:15:36 -04:00
57 changed files with 5714 additions and 0 deletions

63
.gitattributes vendored Normal file
View file

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

402
.gitignore vendored Normal file
View file

@ -0,0 +1,402 @@
DLLs/*
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
LocalOverrides.targets

View 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

View 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>

View 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
)
];
}
}
}

View 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;
}
}
}

View 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;
}
};
}

View 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();
}

View 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
}
}

View 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;
}
}

View file

@ -0,0 +1,6 @@
using RandoConstantGenerators;
namespace Archipelago.HollowKnight;
[GenerateJsonConsts("$.*~", "Data/starts.json")]
public static partial class StartLocationNames { }

View 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" }
};
}
}

View 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();
}
}
}

View 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
}
}
}

View 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;
}
}

View 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);
}
}
}
}

View 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();
}
}
}
}

View 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;
}
}
}
}
}

View 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);
}
}
}

View 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();
}
}
}

View file

@ -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;
}
}

View 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;
}
}
}
}
}
}

View 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");
}
}
}
}

View file

@ -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)
};
}
}

View 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();
}
});
}
}

View 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();
}
}
}
}

View 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."
);
}
}
}

View file

@ -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;
}
}
}
}

View 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;
}
}
}
}
}

View 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
});
}
}
}
}

View 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!

View 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

View 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,
};
}
}

View file

@ -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);
}
}

View 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()
{
}
}
}

View file

@ -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);
}
}

View 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)
{
}
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,3 @@
ItemChanger
MenuChanger
Benchwarp

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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
View 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.

19
Mod/README.md Normal file
View file

@ -0,0 +1,19 @@
# 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).
This mod is a lightly modified fork, the original license is viewable at LICENSE.txt.
## 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.
## 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.