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
{
///
/// Handles the sending and receiving of items from the server
///
public class ItemNetworkingModule : Module
{
///
/// A preset GiveInfo structure that avoids creating geo and places messages in the corner.
///
public static GiveInfo RemoteGiveInfo = new()
{
FlingType = FlingType.DirectDeposit,
Callback = null,
Container = Container.Unknown,
MessageType = MessageType.Corner
};
///
/// A preset GiveInfo structure that avoids creating geo and outputs no messages, e.g. for Start Items.
///
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 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 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();
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();
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."
);
}
}
}