373 lines
14 KiB
C#
373 lines
14 KiB
C#
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."
|
|
);
|
|
}
|
|
}
|
|
}
|