Fix off-by-one errors

Fix off-by-one errors

// Issue #118: This class provides instructions for various game systems
// regarding the handling of any additional items slots we define
// This is used as a way to only have the minumum required code in the Highlander
// and externalize the code that handles those slots into other mods.
// It also abstracts some behavior of vanilla slots into static helper functions

// Many of these functions reference a CheckGameState: This is used to handle Multiplayer more gracefully
// Multiplayer doesn't use a History, but rather a single game state that is never submitted.
// Hence, many functions may need a game state to determine slot behavior. We could have avoided
// using this in multiplayer at all, but it's better to use a single approach
// Mod code is encouraged to make use of this, by not relying on the history alone
// (i.e. use Unit.GetItemInSlot, it can search an existing game state)
class CHItemSlot extends X2DataTemplate;

// These are Slot Categories! In vanilla:
// Item = Utility, Grenade/Ammo Pocket; Weapon = Primary, Secondary, Heavy; Armor = Armor, Heavy Weapon
// A lot of UI code and options rely on these
const SLOT_ARMOR       = 0x00000001; // affected by StripArmor
const SLOT_WEAPON      = 0x00000002; // affected by StripWeapons
const SLOT_ITEM        = 0x00000004; // affected by StripItems
const SLOT_MISC        = 0x00000008; // affected by code that strips all items
const SLOT_ALL         = 0x0000000F; // combined mask for all these slot types

// For the slot unequip behavior, used in UIArmory_Loadout
// Issue #171 -- this receives a change in semantics. These are only used for determining whether to show the drop button.
// This should be backwards compatible, as we required ValidateLoadout to mirror this setting.
// This makes AttemptReEquip and AllowEmpty behave the same way, but both can be used to clarify the intent of the Drop button
enum ECHSlotUnequipBehavior
{
	eCHSUB_AttemptReEquip, // Show a drop button, equivalent to AllowEmpty
	eCHSUB_DontAllow, // Do not show a drop button
	eCHSUB_AllowEmpty, // Show a drop button
};

// There can only be one Template per slot
var EInventorySlot InvSlot;

// Bitfield of the above constants. Only non-zero if this slot can be equipped and unequipped by hand,
// and if MakeItemsAvailable should pick this slot up
var int SlotCatMask;
// should be equivalent to SlotCatMask != 0, keeping it for potential validation
var bool IsUserEquipSlot;

// This slot contains equipment and *may* be subject to
// Class Restrictions for Armor and Weapon Templates
// And the Unique rule potentially only allowing a single type per category
// It does NOT mean that this slot should be displayed for this unit, or that the
// user can manipulate this slot
var bool IsEquippedSlot;

// If this is an equipped slot, this slot is ignored when considering the unique rule
var bool BypassesUniqueRule;

// Can this slot hold more than one item (similar to Utility Slots)?
// Imposes the limitation that its items can not be shown on cinematic pawns (Armory, SquadSelect, tactical Matinee).
var bool IsMultiItemSlot;

// Minimum number of items equipped on this slot, set to -1 to fill until all multi slots is full.
var int MinimumEquipped;

// If this slot is displayed, it can be displayed in a compact format in the SquadSelect screen
// This imposes two limitations:
// 1) Its item 2d images should be higher than wide, and
// 2) Its priority is forced to be lower than any other non-small slot ("small items come last")
// Slots are sorted first by priority, and then by Small-Notsmall
var bool IsSmallSlot;

// This slot contains items that need to be PresEquip()ed in XGUnit::ApplyLoadoutFromGameState()
// The code behind this involves a lot of legacy Inventory code from EU/EW that I'm not entirely comfortable with.
// It's best to follow the vanilla rule for slots involved, which is "only do it for weapons" (primary-septenary + heavy)
// This seems to be mandatory for weapons that have a sheath mesh, as well as gremlins / bits
var bool NeedsPresEquip;

// Items in this slot will be shown in the Armory, in SquadSelect, and tactical Matinees
// Can not be a multi-item slot due to UIPawnMgr restrictions, but other than that, you should se it to
// true for all slots that the user should be able to see on the Unit Pawn (see XComUnitPawn::CreateVisualInventoryAttachments())
var bool ShowOnCinematicPawns;

// DELEGATES
// A LOT OF DELEGATES

// Required
delegate bool CanAddItemToSlotFn(CHItemSlot Slot, XComGameState_Unit Unit, X2ItemTemplate Template, optional XComGameState CheckGameState, optional int Quantity = 1, optional XComGameState_Item ItemState);
delegate bool UnitHasSlotFn(CHItemSlot Slot, XComGameState_Unit UnitState, out string LockedReason, optional XComGameState CheckGameState);
delegate int GetPriorityFn(CHItemSlot Slot, XComGameState_Unit UnitState, optional XComGameState CheckGameState);
// Required if IsMultiItemSlot
delegate int GetMaxItemCountFn(CHItemSlot Slot, XComGameState_Unit UnitState, optional XComGameState CheckGameState);

// Optional
delegate AddItemToSlotFn(CHItemSlot Slot, XComGameState_Unit Unit, XComGameState_Item Item, XComGameState NewGameState);
delegate bool CanRemoveItemFromSlotFn(CHItemSlot Slot, XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState CheckGameState);
delegate RemoveItemFromSlotFn(CHItemSlot Slot, XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState NewGameState);
// Falls back to matching slots
delegate bool ShowItemInLockerListFn(CHItemSlot Slot, XComGameState_Unit Unit, XComGameState_Item ItemState, X2ItemTemplate ItemTemplate, XComGameState CheckGameState);
// ItemState is the Item that is in the slot
// Return eCHSUB_DontAllow to not show a drop button, eCHSUB_AllowEmpty to show one. If another item needs to be equipped, handle in ValidateLoadout
delegate ECHSlotUnequipBehavior GetSlotUnequipBehaviorFn(CHItemSlot Slot, ECHSlotUnequipBehavior DefaultBehavior, XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState CheckGameState);
delegate array<X2EquipmentTemplate> GetBestGearForSlotFn(CHItemSlot Slot, XComGameState_Unit Unit);
delegate ValidateLoadoutFn(CHItemSlot Slot, XComGameState_Unit Unit, XComGameState_HeadquartersXCom XComHQ, XComGameState NewGameState);
// Falls back to class'UIArmory_Loadout'.default.m_strInventoryLabels
delegate string GetDisplayNameFn(CHItemSlot Slot);
// Uses the first character DisplayName, encouraged to override because language doesn't work that way
delegate string GetDisplayLetterFn(CHItemSlot Slot);
// Falls back to UnitHasSlot. This is only used for generic loadout code. For example,
// eInvSlot_CombatSim has this as false, but UIArmory_Implants still allows it to be equipped
// Further, CHUIItemSlotEnumerator can handle this slot if UI explicitely requests it
delegate bool UnitShowSlotFn(CHItemSlot Slot, XComGameState_Unit UnitState, optional XComGameState CheckGameState);
// if IsMultiItemSlot, show an addditional locked slot with an optional tooltip for how to unlock it
// This is only called if this slot is shown at all (via UnitShowSlotFn)
delegate bool GetMultiSlotUnlockHintFn(CHItemSlot Slot, XComGameState_Unit UnitState, out string strReason, optional XComGameState CheckGameState);

// Called from XComGameState_Unit::CanAddItemToInventory if the slot is templated and the Game doesn't know what to do
// Quantity is unused in any of the vanilla slots
function bool CanAddItemToSlot(XComGameState_Unit Unit, X2ItemTemplate Template, optional XComGameState CheckGameState, optional int Quantity = 1, optional XComGameState_Item ItemState)
{
	return CanAddItemToSlotFn(self, Unit, Template,  CheckGameState, Quantity, ItemState);
}

// Called from XComGameState_Unit::AddItemToInventory if the slot is templated and the Game doesn't know what to do
function AddItemToSlot(XComGameState_Unit Unit, XComGameState_Item Item, XComGameState NewGameState)
{
	if (AddItemToSlotFn != none)
	{
		AddItemToSlotFn(self, Unit, Item, NewGameState);
	}
}

// Called from XComGameState_Unit::CanRemoveItemFromInventory if the slot is templated and the Game doesn't know what to do
function bool CanRemoveItemFromSlot(XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState CheckGameState)
{
	if (CanRemoveItemFromSlotFn != none)
	{
		return CanRemoveItemFromSlotFn(self, Unit, ItemState, CheckGameState);
	}
	return true;
}

// Called from XComGameState_Unit::RemoveItemFromInventory if the slot is templated and the Game doesn't know what to do
function RemoveItemFromSlot(XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState NewGameState)
{
	if (RemoveItemFromSlotFn != none)
	{
		RemoveItemFromSlotFn(self, Unit, ItemState, NewGameState);
	}
}

// Called from UIArmory_LoadoutItem to determine whether to show a drop button and whether to attempt re-equipping an now empty slot
function ECHSlotUnequipBehavior GetSlotUnequipBehavior(ECHSlotUnequipBehavior DefaultBehavior, XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState CheckGameState)
{
	if (GetSlotUnequipBehaviorFn != none)
	{
		return GetSlotUnequipBehaviorFn(self, DefaultBehavior, Unit, ItemState, CheckGameState);
	}
	return DefaultBehavior;
}

// Called from XComGameState_Unit::GetBestGearForSlot if the slot is templated and the Game doesn't know what to do
function array<X2EquipmentTemplate> GetBestGearForSlot(XComGameState_Unit Unit)
{
	local array<X2EquipmentTemplate> arr;
	if (GetBestGearForSlotFn != none)
	{
		return GetBestGearForSlotFn(self, Unit);
	}
	arr.Length = 0;
	return arr;
}

// Called from XComGameState_Unit::ValidateLoadout, regardless of whether the unit has the slot or not.
// This can be used to add a missing item, or remove an item the unit shouldn't have
function ValidateLoadout(XComGameState_Unit Unit, XComGameState_HeadquartersXCom XComHQ, XComGameState NewGameState)
{
	if (ValidateLoadoutFn != none)
	{
		ValidateLoadoutFn(self, Unit, XComHQ, NewGameState);
	}
}

// If IsMultiSlot, this gets the number of items this slot can hold (similar to Utility Items)
// If -1, there is no limit on the number of items this slot can hold.
//   In this case, if this slot is displayed, UI should show one (1) empty slot and let the list grow or shrink when items are added / removed
function int GetMaxItemCount(XComGameState_Unit UnitState, optional XComGameState CheckGameState)
{
	return GetMaxItemCountFn(self, UnitState, CheckGameState);
}

// Is non-infinite IsMultiSlot, this function provides an optional empty "locked" slot entry to be shown. Since HasSlot/ShowSlot only works for the
// entire slot, it can't be used to make some slots locked. Additionally, the locked slot would be more than the current MaxItemCount
// UI is not required to implement this, currently this is only used as a formalization of the code in UISquadSelect_ListItem (show a locked utility slot under special circumstances)
function bool GetMultiSlotUnlockHint(XComGameState_Unit UnitState, out string strReason, optional XComGameState CheckGameState)
{
	if (GetMultiSlotUnlockHintFn != none)
	{
		return GetMultiSlotUnlockHintFn(self, UnitState, strReason, CheckGameState);
	}
	strReason = "";
	return false;
}

// Return the name of this slot
function string GetDisplayName()
{
	if (GetDisplayNameFn != none)
	{
		return GetDisplayNameFn(self);
	}
	return class'UIArmory_Loadout'.default.m_strInventoryLabels[int(InvSlot)];
}

// Single letter that abbreviates the slot name
function string GetDisplayLetter()
{
	if (GetDisplayLetterFn != none)
	{
		return GetDisplayLetterFn(self);
	}
	// Inevitably breaks with Unicode characters
	return Left(GetDisplayName(), 1);
}

function bool ShowItemInLockerList(XComGameState_Unit Unit, XComGameState_Item ItemState, X2ItemTemplate ItemTemplate, XComGameState CheckGameState)
{
	local X2EquipmentTemplate EquipmentTemplate;
	if (ShowItemInLockerListFn != none)
	{
		return ShowItemInLockerListFn(self, Unit, ItemState, ItemTemplate, CheckGameState);
	}
	EquipmentTemplate = X2EquipmentTemplate(ItemTemplate);
	// xpad is only item with size 0, that is always equipped
	return (EquipmentTemplate != none && EquipmentTemplate.iItemSize > 0 && EquipmentTemplate.InventorySlot == self.InvSlot);

}

// Return true if the slot should be shown for this unit
function bool UnitShowSlot(XComGameState_Unit UnitState, optional XComGameState CheckGameState)
{
	local string strDummy;
	if (UnitShowSlotFn != none)
	{
		return UnitShowSlotFn(self, UnitState, CheckGameState);
	}
	return UnitHasSlot(UnitState, strDummy, CheckGameState);
}

// Return true if the unit has this slot. This means that this slot is not locked
// If the slot should be shown (see above function) but it's locked, LockedReason should be filled out
function bool UnitHasSlot(XComGameState_Unit UnitState, out string LockedReason, optional XComGameState CheckGameState)
{
	return UnitHasSlotFn(self, UnitState, LockedReason, CheckGameState);
}

// Called from CHUIItemSlotEnumerator to determine a slot order
// Higher number -> appears later. See notes in IsSmallSlot
// For vanilla slots, Priority = int(Slot) * 10;
function int GetPriority(XComGameState_Unit UnitState, optional XComGameState CheckGameState)
{
	return GetPriorityFn(self, UnitState, CheckGameState);
}

// Static Helper Functions

// Returns true if and only if this slot is provided by the vanilla game
// and does not need any custom handling
final static function bool IsVanillaProvidedSlot(EInventorySlot Slot)
{
	return Slot > eInvSlot_Unknown && Slot < eInvSlot_END_VANILLA_SLOTS;
}

// Returns true if and only if this slot is within the templated slot range
final static function bool IsWithinTemplatedSlotRange(EInventorySlot Slot)
{
	return Slot > eInvSlot_BEGIN_TEMPLATED_SLOTS && Slot < eInvSlot_END_TEMPLATED_SLOTS;
}

// Returns true if and only if this slot is within the Templated Slot range and has an existing template
final static function bool SlotIsTemplated(EInventorySlot Slot)
{
	return GetTemplateForSlot(Slot) != none;
}

// Returns the template controlling this slot if it exists, none otherwise
final static function CHItemSlot GetTemplateForSlot(EInventorySlot Slot)
{
	local CHItemSlotStore Store;
	Store = class'CHItemSlotStore'.static.GetStore();
	return Store.GetSlot(Slot);
}

final static function array<CHItemSlot> GetAllSlotTemplates()
{
	return class'CHItemSlotStore'.static.GetStore().GetAllSlotTemplates();
}

// Abstraction functions so we don't have to put the switch() and SlotIsTemplated() everywhere in the code base

static function bool SlotIsSmall(EInventorySlot Slot)
{
	return Slot == eInvSlot_Utility || Slot == eInvSlot_GrenadePocket || Slot == eInvSlot_AmmoPocket || (SlotIsTemplated(Slot) && GetTemplateForSlot(Slot).IsSmallSlot);
}

// Should this slot be shown in generic loadout UI? (UIArmory_Loadout, UISquadSelect)
static function bool SlotShouldBeShown(EInventorySlot Slot, XComGameState_Unit Unit, optional XComGameState CheckGameState)
{
	local string strDummy;
	if (SlotIsTemplated(Slot))
	{
		return GetTemplateForSlot(Slot).UnitShowSlot(Unit, CheckGameState);
	}
	else
	{
		switch (Slot)
		{
			case eInvSlot_Mission:
			case eInvSlot_Backpack:
			case eInvSlot_Loot:
			// CombatSims is hidden as a loadout slot. Special UI that specificially considers CombatSims (SoldierHeader, Armory_Implants, mod code)
			// Can choose to ignore this
			case eInvSlot_CombatSim:
			case eInvSlot_TertiaryWeapon:
			case eInvSlot_QuaternaryWeapon:
			case eInvSlot_QuinaryWeapon:
			case eInvSlot_SenaryWeapon:
			case eInvSlot_SeptenaryWeapon:
				// hidden slots
				return false;
			default:
				return SlotAvailable(Slot, strDummy, Unit, CheckGameState);
		}
	}
	return false;
}

static function bool SlotAvailable(EInventorySlot Slot, out string LockedReason, XComGameState_Unit Unit, optional XComGameState CheckGameState)
{

	LockedReason = "";
	switch (Slot)
	{
		case eInvSlot_Armor:
		case eInvSlot_PrimaryWeapon:
			return true;
		case eInvSlot_SecondaryWeapon:
			return Unit.NeedsSecondaryWeapon();
		case eInvSlot_HeavyWeapon:
			return Unit.GetNumHeavyWeapons(CheckGameState) > 0;
		case eInvSlot_Utility:
		case eInvSlot_CombatSim:
			// Units always have a utility slot, but sometimes eStat_UtilityItems == 0. We consider the slot to be available
			// Same for CombatSim
			return true;
		case eInvSlot_GrenadePocket:
			return Unit.HasGrenadePocket();
		case eInvSlot_AmmoPocket:
			return Unit.HasAmmoPocket();
		default:
			if (SlotIsTemplated(Slot)) {
				return GetTemplateForSlot(Slot).UnitHasSlot(Unit, LockedReason, CheckGameState);
			}
			// Mission, backpack, Loot, Tertiary-Septenary are always available
			return true;
	}
}

static function bool SlotShowItemInLockerList(EInventorySlot Slot, XComGameState_Unit Unit, XComGameState_Item ItemState, X2ItemTemplate ItemTemplate, XComGameState CheckGameState)
{
	local X2GrenadeTemplate GrenadeTemplate;
	local X2EquipmentTemplate EquipmentTemplate;

	switch(Slot)
	{
		case eInvSlot_GrenadePocket:
			GrenadeTemplate = X2GrenadeTemplate(ItemTemplate);
			return GrenadeTemplate != none;
		case eInvSlot_AmmoPocket:
			return ItemTemplate.ItemCat == 'ammo';
		default:
			if (SlotIsTemplated(Slot))
			{
				return GetTemplateForSlot(Slot).ShowItemInLockerList(Unit, ItemState, ItemTemplate, CheckGameState);
			}
			EquipmentTemplate = X2EquipmentTemplate(ItemTemplate);
			// xpad is only item with size 0, that is always equipped
			return (EquipmentTemplate != none && EquipmentTemplate.iItemSize > 0 && EquipmentTemplate.InventorySlot == Slot);
	}
}

static function ECHSlotUnequipBehavior SlotGetUnequipBehavior(EInventorySlot Slot, XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState CheckGameState)
{
	local ECHSlotUnequipBehavior Behavior;
	local XComLWTuple OverrideTuple;

	// Base game behavior: If the item is not infinite or has been modified, show the drop button and attempt to replace it with another item
	// Otherwise, don't show the drop button
	Behavior = (!ItemState.GetMyTemplate().bInfiniteItem ||ItemState.HasBeenModified()) ? eCHSUB_AttemptReEquip : eCHSUB_DontAllow;

	// If the slot is templated, the slot can define whether to let this item be unequipped / replaced. If the slot doesn't implement this,
	// The default behavior is used
	if (SlotIsTemplated(Slot))
	{
		Behavior = GetTemplateForSlot(Slot).GetSlotUnequipBehavior(Behavior, Unit, ItemState, CheckGameState);
	}

	// Add an event trigger from the original Highlander to do it on a per-item basis for arbitrary slots

	// Adapted from the Original Highlander (#89)
	// Instead of a boolean, we use the Enum instead
	//set up a Tuple for return value
	OverrideTuple = new class'XComLWTuple';
	OverrideTuple.Id = 'OverrideItemUnequipBehavior';
	OverrideTuple.Data.Add(3);
	// XComLWTuple does not have a Byte kind
	OverrideTuple.Data[0].kind = XComLWTVInt;
	OverrideTuple.Data[0].i = Behavior;
	OverrideTuple.Data[1].kind = XComLWTVObject;
	OverrideTuple.Data[1].o = Unit;
	OverrideTuple.Data[2].kind = XComLWTVObject;
	OverrideTuple.Data[2].o = CheckGameState;

	`XEVENTMGR.TriggerEvent('OverrideItemUnequipBehavior', OverrideTuple, ItemState);

	return ECHSlotUnequipBehavior(OverrideTuple.Data[0].i);
}

// If UnitHasSlot(), this returns the minimum number of items that need to be in that slot for ValidateLoadout, or -1 for "all"
// Calling code must ensure that the Unit has the slot.
static function int SlotGetMinimumEquipped(EInventorySlot Slot, XComGameState_Unit Unit)
{
	local int MinSlots;
	local XComLWTuple OverrideTuple;

	if (SlotIsTemplated(Slot))
	{
		MinSlots = GetTemplateForSlot(Slot).MinimumEquipped;
	}
	else
	{
		switch (Slot)
		{
			case eInvSlot_SecondaryWeapon:
			case eInvSlot_Armor:
			case eInvSlot_PrimaryWeapon:
			case eInvSlot_GrenadePocket:
			case eInvSlot_Utility:
			case eInvSlot_HeavyWeapon:
				MinSlots = 1;
				break;
			default:
				MinSlots = 0;
				break;
		}
	}

	OverrideTuple = new class'XComLWTuple';
	OverrideTuple.Id = 'OverrideItemMinEquipped';
	OverrideTuple.Data.Add(2);
	OverrideTuple.Data[0].kind = XComLWTVInt;
	OverrideTuple.Data[0].i = MinSlots;
	// XComLWTuple does not have a Byte kind
	OverrideTuple.Data[1].kind = XComLWTVInt;
	OverrideTuple.Data[1].i = int(Slot);

	`XEVENTMGR.TriggerEvent('OverrideItemUnequipBehavior', OverrideTuple, Unit);

	MinSlots = OverrideTuple.Data[0].i;

	return MinSlots;
}

static function int SlotGetPriority(EInventorySlot Slot, XComGameState_Unit Unit, optional XComGameState CheckGameState)
{
	if (IsVanillaProvidedSlot(Slot))
	{
		return 10 * int(Slot);
	}
	else if (SlotIsTemplated(Slot))
	{
		return GetTemplateForSlot(Slot).GetPriority(Unit, CheckGameState);
	}
	return -1;
}

static function string SlotGetName(EInventorySlot Slot)
{
	if (SlotIsTemplated(Slot))
	{
		return GetTemplateForSlot(Slot).GetDisplayName();
	}
	return class'UIArmory_Loadout'.default.m_strInventoryLabels[int(Slot)];
}

static function array<EInventorySlot> GetDefaultDisplayedSlots(XComGameState_Unit Unit, optional XComGameState CheckGameState)
{
	local int i;
	local array<EInventorySlot> Slots;
	for (i = eInvSlot_Unknown + 1; i < eInvSlot_END_VANILLA_SLOTS; i++)
	{
		if (SlotShouldBeShown(EInventorySlot(i), Unit, CheckGameState))
		{
			Slots.AddItem(EInventorySlot(i));
		}
	}

	for (i = eInvSlot_BEGIN_TEMPLATED_SLOTS + 1; i < eInvSlot_END_TEMPLATED_SLOTS; i++)
	{
		// Just because the enum value exists, the slot doesn't necessarily exist
		if (SlotIsTemplated(EInventorySlot(i)) && SlotShouldBeShown(EInventorySlot(i), Unit, CheckGameState))
		{
			Slots.AddItem(EInventorySlot(i));
		}
	}
	return Slots;
}

static function bool SlotIsMultiItem(EInventorySlot Slot)
{
	return Slot == eInvSlot_Backpack || Slot == eInvSlot_Utility || Slot == eInvSlot_HeavyWeapon || Slot == eInvSlot_CombatSim || (SlotIsTemplated(Slot) && GetTemplateForSlot(Slot).IsMultiItemSlot);
}

// Only valid for Multi-Item slots!
// Return -1 for infinite
static function int SlotGetMaxItemCount(EInventorySlot Slot, XComGameState_Unit Unit, optional XComGameState CheckGameState)
{
	if (SlotIsMultiItem(Slot) == false)
	{
		`REDSCREEN(GetFuncName() $ " called with Slot " $ GetEnum(Enum'EInventorySlot', Slot) $ " which is no Multi-item slot!\n" @ GetScriptTrace());
		return 0;
	}
	switch (Slot)
	{
		case eInvSlot_Utility:
			return Unit.IsMPCharacter() ? Unit.GetMPCharacterTemplate().NumUtilitySlots : int(Unit.GetCurrentStat(eStat_UtilityItems));
		case eInvSlot_CombatSim:
			return int(Unit.GetCurrentStat(eStat_CombatSims));
		case eInvSlot_Backpack:
			return -1;
		// Start Issue #171
		case eInvSlot_HeavyWeapon:
			return Unit.GetNumHeavyWeapons(CheckGameState);
		// End Issue #171
		default:
			// Due to the check for SlotIsMultiItem, this slot must be templated
			return GetTemplateForSlot(Slot).GetMaxItemCount(Unit, CheckGameState);
	}
}

static function bool SlotGetMultiUnlockHint(EInventorySlot Slot, XComGameState_Unit Unit, out string strReason, optional XComGameState CheckGameState)
{
	if (Slot == eInvSlot_Utility)
	{
		if (Unit.GetCurrentStat(eStat_UtilityItems) == 0)
		{
			if (Unit.IsResistanceHero())
			{
				// Hero soldiers cannot gain an extra slot via armor, so we just say "can't edit"
				strReason = class'UIArmory_Loadout'.default.m_strCannotEdit; // "Cannot edit"
				return true;
			}
			else
			{
				// If we don't have any utility slots and aren't a resistance hero, we don't have utility slots. Period.
				// (Sparks)
				strReason = class'UISquadSelect_ListItem'.default.m_strNoUtilitySlots; // "No utility slots available"
				return true;
			}
		}
		else if (!Unit.HasExtraUtilitySlot())
		{
			// Normal soldiers that don't have an extra utility slot can gain an additional one via Predator Armor
			// TODO: They can also gain one via Tactical Rigging / equivalent, but we don't have localization for that
			strReason = class'UISquadSelect_ListItem'.default.m_strNeedsMediumArmor; // "Requires advanced armor"
			return true;
		}
	}
	else if (SlotIsTemplated(Slot))
	{
		return GetTemplateForSlot(Slot).GetMultiSlotUnlockHint(Unit, strReason, CheckGameState);
	}
	return false;
}

// Use this to collect slots to MakeItemsAvailable. SlotMask is a Bit Mask from the consts above, SLOT_ALL is a shortcut for SLOT_ARMOR|SLOT_WEAPON|SLOT_ITEM
// Slots are not sorted in any way
static function CollectSlots(int SlotMask, out array<EInventorySlot> Slots)
{
	local array<CHItemSlot> AllSlots;
	local CHItemSlot Slot;

	if ((SlotMask & SLOT_ARMOR) != 0)
	{
		Slots.AddItem(eInvSlot_Armor);
		Slots.AddItem(eInvSlot_HeavyWeapon);
	}
	if ((SlotMask & SLOT_WEAPON) != 0)
	{
		Slots.AddItem(eInvSlot_PrimaryWeapon);
		Slots.AddItem(eInvSlot_SecondaryWeapon);
		Slots.AddItem(eInvSlot_HeavyWeapon);
	}
	if ((SlotMask & SLOT_ITEM) != 0)
	{
		Slots.AddItem(eInvSlot_Utility);
		Slots.AddItem(eInvSlot_GrenadePocket);
		Slots.AddItem(eInvSlot_AmmoPocket);
	}

	AllSlots = GetAllSlotTemplates();
	foreach AllSlots(Slot)
	{
		if ((Slot.SlotCatMask & SlotMask) != 0)
		{
			Slots.AddItem(Slot.InvSlot);
		}
	}
}

// Issue #118: This class provides instructions for various game systems
// regarding the handling of any additional items slots we define
// This is used as a way to only have the minumum required code in the Highlander
// and externalize the code that handles those slots into other mods.
// It also abstracts some behavior of vanilla slots into static helper functions

// Many of these functions reference a CheckGameState: This is used to handle Multiplayer more gracefully
// Multiplayer doesn't use a History, but rather a single game state that is never submitted.
// Hence, many functions may need a game state to determine slot behavior. We could have avoided
// using this in multiplayer at all, but it's better to use a single approach
// Mod code is encouraged to make use of this, by not relying on the history alone
// (i.e. use Unit.GetItemInSlot, it can search an existing game state)
class CHItemSlot extends X2DataTemplate;

// These are Slot Categories! In vanilla:
// Item = Utility, Grenade/Ammo Pocket; Weapon = Primary, Secondary, Heavy; Armor = Armor, Heavy Weapon
// A lot of UI code and options rely on these
const SLOT_ARMOR       = 0x00000001; // affected by StripArmor
const SLOT_WEAPON      = 0x00000002; // affected by StripWeapons
const SLOT_ITEM        = 0x00000004; // affected by StripItems
const SLOT_MISC        = 0x00000008; // affected by code that strips all items
const SLOT_ALL         = 0x0000000F; // combined mask for all these slot types

// For the slot unequip behavior, used in UIArmory_Loadout
// Issue #171 -- this receives a change in semantics. These are only used for determining whether to show the drop button.
// This should be backwards compatible, as we required ValidateLoadout to mirror this setting.
// This makes AttemptReEquip and AllowEmpty behave the same way, but both can be used to clarify the intent of the Drop button
enum ECHSlotUnequipBehavior
{
	eCHSUB_AttemptReEquip, // Show a drop button, equivalent to AllowEmpty
	eCHSUB_DontAllow, // Do not show a drop button
	eCHSUB_AllowEmpty, // Show a drop button
};

// There can only be one Template per slot
var EInventorySlot InvSlot;

// Bitfield of the above constants. Only non-zero if this slot can be equipped and unequipped by hand,
// and if MakeItemsAvailable should pick this slot up
var int SlotCatMask;
// should be equivalent to SlotCatMask != 0, keeping it for potential validation
var bool IsUserEquipSlot;

// This slot contains equipment and *may* be subject to
// Class Restrictions for Armor and Weapon Templates
// And the Unique rule potentially only allowing a single type per category
// It does NOT mean that this slot should be displayed for this unit, or that the
// user can manipulate this slot
var bool IsEquippedSlot;

// If this is an equipped slot, this slot is ignored when considering the unique rule
var bool BypassesUniqueRule;

// Can this slot hold more than one item (similar to Utility Slots)?
// Imposes the limitation that its items can not be shown on cinematic pawns (Armory, SquadSelect, tactical Matinee).
var bool IsMultiItemSlot;

// Minimum number of items equipped on this slot, set to -1 to fill until all multi slots is full.
var int MinimumEquipped;

// If this slot is displayed, it can be displayed in a compact format in the SquadSelect screen
// This imposes two limitations:
// 1) Its item 2d images should be higher than wide, and
// 2) Its priority is forced to be lower than any other non-small slot ("small items come last")
// Slots are sorted first by priority, and then by Small-Notsmall
var bool IsSmallSlot;

// This slot contains items that need to be PresEquip()ed in XGUnit::ApplyLoadoutFromGameState()
// The code behind this involves a lot of legacy Inventory code from EU/EW that I'm not entirely comfortable with.
// It's best to follow the vanilla rule for slots involved, which is "only do it for weapons" (primary-septenary + heavy)
// This seems to be mandatory for weapons that have a sheath mesh, as well as gremlins / bits
var bool NeedsPresEquip;

// Items in this slot will be shown in the Armory, in SquadSelect, and tactical Matinees
// Can not be a multi-item slot due to UIPawnMgr restrictions, but other than that, you should se it to
// true for all slots that the user should be able to see on the Unit Pawn (see XComUnitPawn::CreateVisualInventoryAttachments())
var bool ShowOnCinematicPawns;

// DELEGATES
// A LOT OF DELEGATES

// Required
delegate bool CanAddItemToSlotFn(CHItemSlot Slot, XComGameState_Unit Unit, X2ItemTemplate Template, optional XComGameState CheckGameState, optional int Quantity = 1, optional XComGameState_Item ItemState);
delegate bool UnitHasSlotFn(CHItemSlot Slot, XComGameState_Unit UnitState, out string LockedReason, optional XComGameState CheckGameState);
delegate int GetPriorityFn(CHItemSlot Slot, XComGameState_Unit UnitState, optional XComGameState CheckGameState);
// Required if IsMultiItemSlot
delegate int GetMaxItemCountFn(CHItemSlot Slot, XComGameState_Unit UnitState, optional XComGameState CheckGameState);

// Optional
delegate AddItemToSlotFn(CHItemSlot Slot, XComGameState_Unit Unit, XComGameState_Item Item, XComGameState NewGameState);
delegate bool CanRemoveItemFromSlotFn(CHItemSlot Slot, XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState CheckGameState);
delegate RemoveItemFromSlotFn(CHItemSlot Slot, XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState NewGameState);
// Falls back to matching slots
delegate bool ShowItemInLockerListFn(CHItemSlot Slot, XComGameState_Unit Unit, XComGameState_Item ItemState, X2ItemTemplate ItemTemplate, XComGameState CheckGameState);
// ItemState is the Item that is in the slot
// Return eCHSUB_DontAllow to not show a drop button, eCHSUB_AllowEmpty to show one. If another item needs to be equipped, handle in ValidateLoadout
delegate ECHSlotUnequipBehavior GetSlotUnequipBehaviorFn(CHItemSlot Slot, ECHSlotUnequipBehavior DefaultBehavior, XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState CheckGameState);
delegate array<X2EquipmentTemplate> GetBestGearForSlotFn(CHItemSlot Slot, XComGameState_Unit Unit);
delegate ValidateLoadoutFn(CHItemSlot Slot, XComGameState_Unit Unit, XComGameState_HeadquartersXCom XComHQ, XComGameState NewGameState);
// Falls back to class'UIArmory_Loadout'.default.m_strInventoryLabels
delegate string GetDisplayNameFn(CHItemSlot Slot);
// Uses the first character DisplayName, encouraged to override because language doesn't work that way
delegate string GetDisplayLetterFn(CHItemSlot Slot);
// Falls back to UnitHasSlot. This is only used for generic loadout code. For example,
// eInvSlot_CombatSim has this as false, but UIArmory_Implants still allows it to be equipped
// Further, CHUIItemSlotEnumerator can handle this slot if UI explicitely requests it
delegate bool UnitShowSlotFn(CHItemSlot Slot, XComGameState_Unit UnitState, optional XComGameState CheckGameState);
// if IsMultiItemSlot, show an addditional locked slot with an optional tooltip for how to unlock it
// This is only called if this slot is shown at all (via UnitShowSlotFn)
delegate bool GetMultiSlotUnlockHintFn(CHItemSlot Slot, XComGameState_Unit UnitState, out string strReason, optional XComGameState CheckGameState);

// Called from XComGameState_Unit::CanAddItemToInventory if the slot is templated and the Game doesn't know what to do
// Quantity is unused in any of the vanilla slots
function bool CanAddItemToSlot(XComGameState_Unit Unit, X2ItemTemplate Template, optional XComGameState CheckGameState, optional int Quantity = 1, optional XComGameState_Item ItemState)
{
	return CanAddItemToSlotFn(self, Unit, Template,  CheckGameState, Quantity, ItemState);
}

// Called from XComGameState_Unit::AddItemToInventory if the slot is templated and the Game doesn't know what to do
function AddItemToSlot(XComGameState_Unit Unit, XComGameState_Item Item, XComGameState NewGameState)
{
	if (AddItemToSlotFn != none)
	{
		AddItemToSlotFn(self, Unit, Item, NewGameState);
	}
}

// Called from XComGameState_Unit::CanRemoveItemFromInventory if the slot is templated and the Game doesn't know what to do
function bool CanRemoveItemFromSlot(XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState CheckGameState)
{
	if (CanRemoveItemFromSlotFn != none)
	{
		return CanRemoveItemFromSlotFn(self, Unit, ItemState, CheckGameState);
	}
	return true;
}

// Called from XComGameState_Unit::RemoveItemFromInventory if the slot is templated and the Game doesn't know what to do
function RemoveItemFromSlot(XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState NewGameState)
{
	if (RemoveItemFromSlotFn != none)
	{
		RemoveItemFromSlotFn(self, Unit, ItemState, NewGameState);
	}
}

// Called from UIArmory_LoadoutItem to determine whether to show a drop button and whether to attempt re-equipping an now empty slot
function ECHSlotUnequipBehavior GetSlotUnequipBehavior(ECHSlotUnequipBehavior DefaultBehavior, XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState CheckGameState)
{
	if (GetSlotUnequipBehaviorFn != none)
	{
		return GetSlotUnequipBehaviorFn(self, DefaultBehavior, Unit, ItemState, CheckGameState);
	}
	return DefaultBehavior;
}

// Called from XComGameState_Unit::GetBestGearForSlot if the slot is templated and the Game doesn't know what to do
function array<X2EquipmentTemplate> GetBestGearForSlot(XComGameState_Unit Unit)
{
	local array<X2EquipmentTemplate> arr;
	if (GetBestGearForSlotFn != none)
	{
		return GetBestGearForSlotFn(self, Unit);
	}
	arr.Length = 0;
	return arr;
}

// Called from XComGameState_Unit::ValidateLoadout, regardless of whether the unit has the slot or not.
// This can be used to add a missing item, or remove an item the unit shouldn't have
function ValidateLoadout(XComGameState_Unit Unit, XComGameState_HeadquartersXCom XComHQ, XComGameState NewGameState)
{
	if (ValidateLoadoutFn != none)
	{
		ValidateLoadoutFn(self, Unit, XComHQ, NewGameState);
	}
}

// If IsMultiSlot, this gets the number of items this slot can hold (similar to Utility Items)
// If -1, there is no limit on the number of items this slot can hold.
//   In this case, if this slot is displayed, UI should show one (1) empty slot and let the list grow or shrink when items are added / removed
function int GetMaxItemCount(XComGameState_Unit UnitState, optional XComGameState CheckGameState)
{
	return GetMaxItemCountFn(self, UnitState, CheckGameState);
}

// Is non-infinite IsMultiSlot, this function provides an optional empty "locked" slot entry to be shown. Since HasSlot/ShowSlot only works for the
// entire slot, it can't be used to make some slots locked. Additionally, the locked slot would be more than the current MaxItemCount
// UI is not required to implement this, currently this is only used as a formalization of the code in UISquadSelect_ListItem (show a locked utility slot under special circumstances)
function bool GetMultiSlotUnlockHint(XComGameState_Unit UnitState, out string strReason, optional XComGameState CheckGameState)
{
	if (GetMultiSlotUnlockHintFn != none)
	{
		return GetMultiSlotUnlockHintFn(self, UnitState, strReason, CheckGameState);
	}
	strReason = "";
	return false;
}

// Return the name of this slot
function string GetDisplayName()
{
	if (GetDisplayNameFn != none)
	{
		return GetDisplayNameFn(self);
	}
	return class'UIArmory_Loadout'.default.m_strInventoryLabels[int(InvSlot)];
}

// Single letter that abbreviates the slot name
function string GetDisplayLetter()
{
	if (GetDisplayLetterFn != none)
	{
		return GetDisplayLetterFn(self);
	}
	// Inevitably breaks with Unicode characters
	return Left(GetDisplayName(), 1);
}

function bool ShowItemInLockerList(XComGameState_Unit Unit, XComGameState_Item ItemState, X2ItemTemplate ItemTemplate, XComGameState CheckGameState)
{
	local X2EquipmentTemplate EquipmentTemplate;
	if (ShowItemInLockerListFn != none)
	{
		return ShowItemInLockerListFn(self, Unit, ItemState, ItemTemplate, CheckGameState);
	}
	EquipmentTemplate = X2EquipmentTemplate(ItemTemplate);
	// xpad is only item with size 0, that is always equipped
	return (EquipmentTemplate != none && EquipmentTemplate.iItemSize > 0 && EquipmentTemplate.InventorySlot == self.InvSlot);

}

// Return true if the slot should be shown for this unit
function bool UnitShowSlot(XComGameState_Unit UnitState, optional XComGameState CheckGameState)
{
	local string strDummy;
	if (UnitShowSlotFn != none)
	{
		return UnitShowSlotFn(self, UnitState, CheckGameState);
	}
	return UnitHasSlot(UnitState, strDummy, CheckGameState);
}

// Return true if the unit has this slot. This means that this slot is not locked
// If the slot should be shown (see above function) but it's locked, LockedReason should be filled out
function bool UnitHasSlot(XComGameState_Unit UnitState, out string LockedReason, optional XComGameState CheckGameState)
{
	return UnitHasSlotFn(self, UnitState, LockedReason, CheckGameState);
}

// Called from CHUIItemSlotEnumerator to determine a slot order
// Higher number -> appears later. See notes in IsSmallSlot
// For vanilla slots, Priority = int(Slot) * 10;
function int GetPriority(XComGameState_Unit UnitState, optional XComGameState CheckGameState)
{
	return GetPriorityFn(self, UnitState, CheckGameState);
}

// Static Helper Functions

// Returns true if and only if this slot is provided by the vanilla game
// and does not need any custom handling
final static function bool IsVanillaProvidedSlot(EInventorySlot Slot)
{
	return Slot > eInvSlot_Unknown && Slot < eInvSlot_END_VANILLA_SLOTS;
}

// Returns true if and only if this slot is within the templated slot range
final static function bool IsWithinTemplatedSlotRange(EInventorySlot Slot)
{
	return Slot > eInvSlot_BEGIN_TEMPLATED_SLOTS && Slot < eInvSlot_END_TEMPLATED_SLOTS;
}

// Returns true if and only if this slot is within the Templated Slot range and has an existing template
final static function bool SlotIsTemplated(EInventorySlot Slot)
{
	return GetTemplateForSlot(Slot) != none;
}

// Returns the template controlling this slot if it exists, none otherwise
final static function CHItemSlot GetTemplateForSlot(EInventorySlot Slot)
{
	local CHItemSlotStore Store;
	Store = class'CHItemSlotStore'.static.GetStore();
	return Store.GetSlot(Slot);
}

final static function array<CHItemSlot> GetAllSlotTemplates()
{
	return class'CHItemSlotStore'.static.GetStore().GetAllSlotTemplates();
}

// Abstraction functions so we don't have to put the switch() and SlotIsTemplated() everywhere in the code base

static function bool SlotIsSmall(EInventorySlot Slot)
{
	return Slot == eInvSlot_Utility || Slot == eInvSlot_GrenadePocket || Slot == eInvSlot_AmmoPocket || (SlotIsTemplated(Slot) && GetTemplateForSlot(Slot).IsSmallSlot);
}

// Should this slot be shown in generic loadout UI? (UIArmory_Loadout, UISquadSelect)
static function bool SlotShouldBeShown(EInventorySlot Slot, XComGameState_Unit Unit, optional XComGameState CheckGameState)
{
	local string strDummy;
	if (SlotIsTemplated(Slot))
	{
		return GetTemplateForSlot(Slot).UnitShowSlot(Unit, CheckGameState);
	}
	else
	{
		switch (Slot)
		{
			case eInvSlot_Mission:
			case eInvSlot_Backpack:
			case eInvSlot_Loot:
			// CombatSims is hidden as a loadout slot. Special UI that specificially considers CombatSims (SoldierHeader, Armory_Implants, mod code)
			// Can choose to ignore this
			case eInvSlot_CombatSim:
			case eInvSlot_TertiaryWeapon:
			case eInvSlot_QuaternaryWeapon:
			case eInvSlot_QuinaryWeapon:
			case eInvSlot_SenaryWeapon:
			case eInvSlot_SeptenaryWeapon:
				// hidden slots
				return false;
			default:
				return SlotAvailable(Slot, strDummy, Unit, CheckGameState);
		}
	}
	return false;
}

static function bool SlotAvailable(EInventorySlot Slot, out string LockedReason, XComGameState_Unit Unit, optional XComGameState CheckGameState)
{

	LockedReason = "";
	switch (Slot)
	{
		case eInvSlot_Armor:
		case eInvSlot_PrimaryWeapon:
			return true;
		case eInvSlot_SecondaryWeapon:
			return Unit.NeedsSecondaryWeapon();
		case eInvSlot_HeavyWeapon:
			return Unit.GetNumHeavyWeapons(CheckGameState) > 0;
		case eInvSlot_Utility:
		case eInvSlot_CombatSim:
			// Units always have a utility slot, but sometimes eStat_UtilityItems == 0. We consider the slot to be available
			// Same for CombatSim
			return true;
		case eInvSlot_GrenadePocket:
			return Unit.HasGrenadePocket();
		case eInvSlot_AmmoPocket:
			return Unit.HasAmmoPocket();
		default:
			if (SlotIsTemplated(Slot)) {
				return GetTemplateForSlot(Slot).UnitHasSlot(Unit, LockedReason, CheckGameState);
			}
			// Mission, backpack, Loot, Tertiary-Septenary are always available
			return true;
	}
}

static function bool SlotShowItemInLockerList(EInventorySlot Slot, XComGameState_Unit Unit, XComGameState_Item ItemState, X2ItemTemplate ItemTemplate, XComGameState CheckGameState)
{
	local X2GrenadeTemplate GrenadeTemplate;
	local X2EquipmentTemplate EquipmentTemplate;

	switch(Slot)
	{
		case eInvSlot_GrenadePocket:
			GrenadeTemplate = X2GrenadeTemplate(ItemTemplate);
			return GrenadeTemplate != none;
		case eInvSlot_AmmoPocket:
			return ItemTemplate.ItemCat == 'ammo';
		default:
			if (SlotIsTemplated(Slot))
			{
				return GetTemplateForSlot(Slot).ShowItemInLockerList(Unit, ItemState, ItemTemplate, CheckGameState);
			}
			EquipmentTemplate = X2EquipmentTemplate(ItemTemplate);
			// xpad is only item with size 0, that is always equipped
			return (EquipmentTemplate != none && EquipmentTemplate.iItemSize > 0 && EquipmentTemplate.InventorySlot == Slot);
	}
}

static function ECHSlotUnequipBehavior SlotGetUnequipBehavior(EInventorySlot Slot, XComGameState_Unit Unit, XComGameState_Item ItemState, optional XComGameState CheckGameState)
{
	local ECHSlotUnequipBehavior Behavior;
	local XComLWTuple OverrideTuple;

	// Base game behavior: If the item is not infinite or has been modified, show the drop button and attempt to replace it with another item
	// Otherwise, don't show the drop button
	Behavior = (!ItemState.GetMyTemplate().bInfiniteItem ||ItemState.HasBeenModified()) ? eCHSUB_AttemptReEquip : eCHSUB_DontAllow;

	// If the slot is templated, the slot can define whether to let this item be unequipped / replaced. If the slot doesn't implement this,
	// The default behavior is used
	if (SlotIsTemplated(Slot))
	{
		Behavior = GetTemplateForSlot(Slot).GetSlotUnequipBehavior(Behavior, Unit, ItemState, CheckGameState);
	}

	// Add an event trigger from the original Highlander to do it on a per-item basis for arbitrary slots

	// Adapted from the Original Highlander (#89)
	// Instead of a boolean, we use the Enum instead
	//set up a Tuple for return value
	OverrideTuple = new class'XComLWTuple';
	OverrideTuple.Id = 'OverrideItemUnequipBehavior';
	OverrideTuple.Data.Add(2);
	// XComLWTuple does not have a Byte kind
	OverrideTuple.Data[0].kind = XComLWTVInt;
	OverrideTuple.Data[0].i = Behavior;
	OverrideTuple.Data[1].kind = XComLWTVObject;
	OverrideTuple.Data[1].o = Unit;
	OverrideTuple.Data[2].kind = XComLWTVObject;
	OverrideTuple.Data[2].o = CheckGameState;

	`XEVENTMGR.TriggerEvent('OverrideItemUnequipBehavior', OverrideTuple, ItemState);

	return ECHSlotUnequipBehavior(OverrideTuple.Data[0].i);
}

// If UnitHasSlot(), this returns the minimum number of items that need to be in that slot for ValidateLoadout, or -1 for "all"
// Calling code must ensure that the Unit has the slot.
static function int SlotGetMinimumEquipped(EInventorySlot Slot, XComGameState_Unit Unit)
{
	local int MinSlots;
	local XComLWTuple OverrideTuple;

	if (SlotIsTemplated(Slot))
	{
		MinSlots = GetTemplateForSlot(Slot).MinimumEquipped;
	}
	else
	{
		switch (Slot)
		{
			case eInvSlot_SecondaryWeapon:
			case eInvSlot_Armor:
			case eInvSlot_PrimaryWeapon:
			case eInvSlot_GrenadePocket:
			case eInvSlot_Utility:
			case eInvSlot_HeavyWeapon:
				MinSlots = 1;
				break;
			default:
				MinSlots = 0;
				break;
		}
	}

	OverrideTuple = new class'XComLWTuple';
	OverrideTuple.Id = 'OverrideItemMinEquipped';
	OverrideTuple.Data.Add(1);
	OverrideTuple.Data[0].kind = XComLWTVInt;
	OverrideTuple.Data[0].i = MinSlots;
	// XComLWTuple does not have a Byte kind
	OverrideTuple.Data[1].kind = XComLWTVInt;
	OverrideTuple.Data[1].i = int(Slot);

	`XEVENTMGR.TriggerEvent('OverrideItemUnequipBehavior', OverrideTuple, Unit);

	MinSlots = OverrideTuple.Data[0].i;

	return MinSlots;
}

static function int SlotGetPriority(EInventorySlot Slot, XComGameState_Unit Unit, optional XComGameState CheckGameState)
{
	if (IsVanillaProvidedSlot(Slot))
	{
		return 10 * int(Slot);
	}
	else if (SlotIsTemplated(Slot))
	{
		return GetTemplateForSlot(Slot).GetPriority(Unit, CheckGameState);
	}
	return -1;
}

static function string SlotGetName(EInventorySlot Slot)
{
	if (SlotIsTemplated(Slot))
	{
		return GetTemplateForSlot(Slot).GetDisplayName();
	}
	return class'UIArmory_Loadout'.default.m_strInventoryLabels[int(Slot)];
}

static function array<EInventorySlot> GetDefaultDisplayedSlots(XComGameState_Unit Unit, optional XComGameState CheckGameState)
{
	local int i;
	local array<EInventorySlot> Slots;
	for (i = eInvSlot_Unknown + 1; i < eInvSlot_END_VANILLA_SLOTS; i++)
	{
		if (SlotShouldBeShown(EInventorySlot(i), Unit, CheckGameState))
		{
			Slots.AddItem(EInventorySlot(i));
		}
	}

	for (i = eInvSlot_BEGIN_TEMPLATED_SLOTS + 1; i < eInvSlot_END_TEMPLATED_SLOTS; i++)
	{
		// Just because the enum value exists, the slot doesn't necessarily exist
		if (SlotIsTemplated(EInventorySlot(i)) && SlotShouldBeShown(EInventorySlot(i), Unit, CheckGameState))
		{
			Slots.AddItem(EInventorySlot(i));
		}
	}
	return Slots;
}

static function bool SlotIsMultiItem(EInventorySlot Slot)
{
	return Slot == eInvSlot_Backpack || Slot == eInvSlot_Utility || Slot == eInvSlot_HeavyWeapon || Slot == eInvSlot_CombatSim || (SlotIsTemplated(Slot) && GetTemplateForSlot(Slot).IsMultiItemSlot);
}

// Only valid for Multi-Item slots!
// Return -1 for infinite
static function int SlotGetMaxItemCount(EInventorySlot Slot, XComGameState_Unit Unit, optional XComGameState CheckGameState)
{
	if (SlotIsMultiItem(Slot) == false)
	{
		`REDSCREEN(GetFuncName() $ " called with Slot " $ GetEnum(Enum'EInventorySlot', Slot) $ " which is no Multi-item slot!\n" @ GetScriptTrace());
		return 0;
	}
	switch (Slot)
	{
		case eInvSlot_Utility:
			return Unit.IsMPCharacter() ? Unit.GetMPCharacterTemplate().NumUtilitySlots : int(Unit.GetCurrentStat(eStat_UtilityItems));
		case eInvSlot_CombatSim:
			return int(Unit.GetCurrentStat(eStat_CombatSims));
		case eInvSlot_Backpack:
			return -1;
		// Start Issue #171
		case eInvSlot_HeavyWeapon:
			return Unit.GetNumHeavyWeapons(CheckGameState);
		// End Issue #171
		default:
			// Due to the check for SlotIsMultiItem, this slot must be templated
			return GetTemplateForSlot(Slot).GetMaxItemCount(Unit, CheckGameState);
	}
}

static function bool SlotGetMultiUnlockHint(EInventorySlot Slot, XComGameState_Unit Unit, out string strReason, optional XComGameState CheckGameState)
{
	if (Slot == eInvSlot_Utility)
	{
		if (Unit.GetCurrentStat(eStat_UtilityItems) == 0)
		{
			if (Unit.IsResistanceHero())
			{
				// Hero soldiers cannot gain an extra slot via armor, so we just say "can't edit"
				strReason = class'UIArmory_Loadout'.default.m_strCannotEdit; // "Cannot edit"
				return true;
			}
			else
			{
				// If we don't have any utility slots and aren't a resistance hero, we don't have utility slots. Period.
				// (Sparks)
				strReason = class'UISquadSelect_ListItem'.default.m_strNoUtilitySlots; // "No utility slots available"
				return true;
			}
		}
		else if (!Unit.HasExtraUtilitySlot())
		{
			// Normal soldiers that don't have an extra utility slot can gain an additional one via Predator Armor
			// TODO: They can also gain one via Tactical Rigging / equivalent, but we don't have localization for that
			strReason = class'UISquadSelect_ListItem'.default.m_strNeedsMediumArmor; // "Requires advanced armor"
			return true;
		}
	}
	else if (SlotIsTemplated(Slot))
	{
		return GetTemplateForSlot(Slot).GetMultiSlotUnlockHint(Unit, strReason, CheckGameState);
	}
	return false;
}

// Use this to collect slots to MakeItemsAvailable. SlotMask is a Bit Mask from the consts above, SLOT_ALL is a shortcut for SLOT_ARMOR|SLOT_WEAPON|SLOT_ITEM
// Slots are not sorted in any way
static function CollectSlots(int SlotMask, out array<EInventorySlot> Slots)
{
	local array<CHItemSlot> AllSlots;
	local CHItemSlot Slot;

	if ((SlotMask & SLOT_ARMOR) != 0)
	{
		Slots.AddItem(eInvSlot_Armor);
		Slots.AddItem(eInvSlot_HeavyWeapon);
	}
	if ((SlotMask & SLOT_WEAPON) != 0)
	{
		Slots.AddItem(eInvSlot_PrimaryWeapon);
		Slots.AddItem(eInvSlot_SecondaryWeapon);
		Slots.AddItem(eInvSlot_HeavyWeapon);
	}
	if ((SlotMask & SLOT_ITEM) != 0)
	{
		Slots.AddItem(eInvSlot_Utility);
		Slots.AddItem(eInvSlot_GrenadePocket);
		Slots.AddItem(eInvSlot_AmmoPocket);
	}

	AllSlots = GetAllSlotTemplates();
	foreach AllSlots(Slot)
	{
		if ((Slot.SlotCatMask & SlotMask) != 0)
		{
			Slots.AddItem(Slot.InvSlot);
		}
	}
}


Add default replacements to pre-game/UI

Add default replacements to pre-game/UI

class TestCentralWeaponReplacement extends UTMutator
	dependson(TestCWRUI)
	config(TestCentralWeaponReplacement);

//**********************************************************************************
// Structs
//**********************************************************************************

struct ReplacementIgnoreClassesInfo
{
	/** class name of the pickup we want to ignore to be replaced */
	var name ClassName;
	/** Whether to check for subclasses  */
	var bool bSubClasses;

	structdefaultproperties
	{
		bSubClasses=true
	}
};

struct ReplacementOptionsInfo
{
	/** Whether to replace/remove the weapon */
	var bool bReplaceWeapon;
	/** Whether to check for subclasses  */
	var bool bSubClasses;
	/** Whether to add the weapon to the weapon lockers */
	var bool bAddToLocker;
	/** Whether to add the inventory item to the default inventory (on spawn) */
	var bool bAddToDefault;

	/** Whether to prevent replacing default inventory items (to be used with bSubClasses=true) */
	var bool bNoDefaultInventory<EditCondition=bSubClasses>;

	/** List of classes to ignore in checking for subclasses */
	var array<ReplacementIgnoreClassesInfo> IgnoreSubClasses<EditCondition=bSubClasses>;

	structdefaultproperties
	{
		bReplaceWeapon=true
		bSubClasses=false
		bAddToLocker=false
		bAddToDefault=false
		bNoDefaultInventory=false
	}
};

struct ReplacementInfoEx
{
	/** class name of the weapon we want to get rid of */
	var name OldClassName;
	/** fully qualified path of the class to replace it with */
	var string NewClassPath;

	/** the options for this item */
	var ReplacementOptionsInfo Options;

	/** the mutator/object which has registered the replacement
	(might be none if set by default configuration) */
	var transient Object Registrar;
};

struct TemplateInfo
{
	/** class name of the weapon we want to get rid of */
	var name OldClassName;
	/** fully qualified path of the class to replace it with */
	var string NewClassPath;

	/** the options for this item */
	var ReplacementOptionsInfo Options;

	/** Flag. Set whether to append the package name (of the current package) to the NewClassPath field */
	var bool AddPackage;
};

//**********************************************************************************
// Variables
//**********************************************************************************

var() const string ParameterProfile;

// Pre Game
// ------------

/** @ignore */
var private transient config array<ReplacementInfoEx> StaticWeaponsToReplace;
/** @ignore */
var private transient config array<ReplacementInfoEx> StaticAmmoToReplace;

// Config
// ------------

var config array<ReplacementInfoEx> DefaultWeaponsToReplace;
var config array<ReplacementInfoEx> DefaultAmmoToReplace;

// Workflow
// ------------

var array<ReplacementInfoEx> WeaponsToReplace;
var array<ReplacementInfoEx> AmmoToReplace;

var TestCWRUI DataCache;
var array<ErrorMessageInfo> ErrorMessages;

var array<int> EnforcerIndizes;

var string CurrentProfileName;
var TestCWRMapProfile CurrentProfile;

//**********************************************************************************
// Inherited functions
//**********************************************************************************

// Intialize the default replacments
event PreBeginPlay()
{
	local int i;
	super.PreBeginPlay();

	if (!IsPendingKill())
	{
		WeaponsToReplace.Length = 0;
		AmmoToReplace.Length = 0;

		for (i=0; i<DefaultWeaponsToReplace.Length; i++)
			RegisterWeaponReplacement(none, DefaultWeaponsToReplace[i].OldClassName, DefaultWeaponsToReplace[i].NewClassPath, false, DefaultWeaponsToReplace[i].Options);

		for (i=0; i<DefaultAmmoToReplace.Length; i++)
			RegisterWeaponReplacement(none, DefaultAmmoToReplace[i].OldClassName, DefaultAmmoToReplace[i].NewClassPath, true, DefaultAmmoToReplace[i].Options);
	}
}

// used to check for default inventory items to be replaced or
// whether an item should be added to the weapon lockers or the default inventory
function InitMutator(string Options, out string ErrorMessage)
{
	local int i, j;
	local UTGame G;
	local class<UTWeapon> WeaponClass;
	local TestCWRMapProfile MapProvider;

	super.InitMutator(Options, ErrorMessage);

	if (class'GameInfo'.static.HasOption(Options, ParameterProfile))
	{
		CurrentProfileName = class'GameInfo'.static.ParseOption(Options, ParameterProfile);
	}

	if (CurrentProfileName != "" && class'TestCWRMapProfile'.static.GetMapProfileByName(CurrentProfileName, MapProvider))
	{
		CurrentProfile = MapProvider;
		RegisterWeaponReplacementArray(CurrentProfile, CurrentProfile.WeaponsToReplace, false);
		RegisterWeaponReplacementArray(CurrentProfile, CurrentProfile.AmmoToReplace, true);
	}

	// Make sure the game does not hold a null reference
	G = UTGame(WorldInfo.Game);
	if(G != none)
	{
		for (j=0; j<WeaponsToReplace.Length; j++)
		{
			if (WeaponsToReplace[j].Options.bReplaceWeapon && !WeaponsToReplace[j].Options.bNoDefaultInventory)
			{
				for (i=0; i<G.DefaultInventory.length; i++)
				{
					if (G.DefaultInventory[i] == None) continue;
					if (!WeaponsToReplace[j].Options.bSubClasses && G.DefaultInventory[i].Name != WeaponsToReplace[j].OldClassName) continue;

					// IsA doesn't work for abstract classes, we need to use the workaround/hackfix
					if (WeaponsToReplace[j].Options.bSubClasses && !IsSubClass(G.DefaultInventory[i], WeaponsToReplace[j].OldClassName)) continue;

					if (!ClassPathValid(WeaponsToReplace[j].NewClassPath))
					{
						// replace with nothing
						G.DefaultInventory.Remove(i, 1);
						i--;
						continue;
					}
					else
					{
						WeaponClass = class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[j].NewClassPath, class'Class'));
						if (WeaponsToReplace[j].Options.bReplaceWeapon)
						{
							G.DefaultInventory[i] = WeaponClass;
						}
					}
				}
			}

			if (WeaponsToReplace[j].Options.bAddToDefault && ClassPathValid(WeaponsToReplace[j].NewClassPath))
			{
				WeaponClass = class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[j].NewClassPath, class'Class'));
				if (WeaponClass != none) G.DefaultInventory.AddItem(WeaponClass);
			}

			if (WeaponsToReplace[j].Options.bAddToLocker)
			{
				WeaponClass = class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[j].NewClassPath, class'Class'));
				AddToLocker(WeaponClass);
			}
		}

		if (G.TranslocatorClass != None)
		{
			j = WeaponsToReplace.Find('OldClassName', G.TranslocatorClass.Name);
			if (j != INDEX_NONE)
			{
				if (WeaponsToReplace[j].NewClassPath == "" || !WeaponsToReplace[j].Options.bReplaceWeapon)
				{
					// replace with nothing
					G.TranslocatorClass = None;
				}
				else
				{
					G.TranslocatorClass = class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[j].NewClassPath, class'Class'));
				}
			}
		}

		EnforcerIndizes.Length = 0;
		for (i=0; i<G.DefaultInventory.length; i++)
		{
			if (IsSubClass(G.DefaultInventory[i], 'UTWeap_Enforcer'))
			{
				EnforcerIndizes.AddItem(i);
			}
		}
	}
}

// check for factory and replace/remove their inventory item (if desired)
function bool CheckReplacement(Actor Other)
{
	local UTWeaponPickupFactory WeaponPickup;
	local UTWeaponLocker Locker;
	local UTAmmoPickupFactory AmmoPickup, NewAmmo;
	local int i, Index;
	local class<UTAmmoPickupFactory> NewAmmoClass;

	WeaponPickup = UTWeaponPickupFactory(Other);
	if (WeaponPickup != None)
	{
		if (WeaponPickup.WeaponPickupClass != None)
		{
			if (ShouldBeReplaced(index, WeaponPickup.WeaponPickupClass, false) && WeaponsToReplace[index].Options.bReplaceWeapon)
			{
				if (WeaponsToReplace[index].NewClassPath == "")
				{
					// replace with nothing
					return false;
				}
				WeaponPickup.WeaponPickupClass = class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[index].NewClassPath, class'Class'));
				WeaponPickup.InitializePickup();
			}
		}
	}
	else
	{
		if (UTWeaponLocker(Other) != None)
		{
			Locker = UTWeaponLocker(Other);
			for (i = 0; i < Locker.Weapons.length; i++)
			{
				if (Locker.Weapons[i].WeaponClass != none && ShouldBeReplaced(index, Locker.Weapons[i].WeaponClass, false) && WeaponsToReplace[index].Options.bReplaceWeapon)
				{
					if (WeaponsToReplace[index].NewClassPath == "")
					{
						// replace with nothing
						Locker.ReplaceWeapon(i, None);
					}
					else
					{
						Locker.ReplaceWeapon(i, class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[index].NewClassPath, class'Class')));
					}
				}
			}
		}
		else if (UTAmmoPickupFactory(Other) != none)
		{
			AmmoPickup = UTAmmoPickupFactory(Other);
			if (AmmoPickup.Class != none)
			{
				if (ShouldBeReplaced(index, AmmoPickup.Class, true) && AmmoToReplace[index].Options.bReplaceWeapon)
				{
					if (AmmoToReplace[index].NewClassPath == "")
					{
						// replace with nothing
						return false;
					}
					NewAmmoClass = class<UTAmmoPickupFactory>(DynamicLoadObject(AmmoToReplace[index].NewClassPath, class'Class'));
					if (NewAmmoClass == None)
					{
						// replace with nothing
						return false;
					}
					else if (NewAmmoClass.default.bStatic || NewAmmoClass.default.bNoDelete)
					{
						// transform the current ammo into the desired class
						AmmoPickup.TransformAmmoType(NewAmmoClass);
						return true;
					}
					else
					{
						// spawn the new ammo, link it to the old, then disable the old one
						NewAmmo = AmmoPickup.Spawn(NewAmmoClass);
						NewAmmo.OriginalFactory = AmmoPickup;
						AmmoPickup.ReplacementFactory = NewAmmo;
						return false;
					}
				}
			}
		}

		// remove initial anim for Enforcers (may only work on server/listen player)
		else if (EnforcerIndizes.Length > 1 && UTWeap_Enforcer(Other) != none)
		{
			UTWeap_Enforcer(Other).bLoaded = true;
			UTWeap_Enforcer(Other).EquipTime = UTWeap_Enforcer(Other).ReloadTime;
		}
	}

	return true;
}

// Fix Dual Enforcer
/** called by GameInfo.RestartPlayer() */
function ModifyPlayer(Pawn Other)
{
	local int i, index;
	local UTWeap_Enforcer Enf;
	super.ModifyPlayer(Other);

	// special case for Akimbo (Enforcer only)
	if (EnforcerIndizes.Length > 1 && UTGame(WorldInfo.Game) != none)
	{
		index = EnforcerIndizes[0];
		Enf = UTWeap_Enforcer(Other.FindInventoryType(UTGame(WorldInfo.Game).DefaultInventory[0]));
		if (Enf != none)
		{
			for (i=1; i<EnforcerIndizes.Length; i++)
			{
				index = EnforcerIndizes[i];
				if (index < UTGame(WorldInfo.Game).DefaultInventory.Length)
				{
					if (!Enf.DenyPickupQuery(UTGame(WorldInfo.Game).DefaultInventory[index], none))
					{
						Other.CreateInventory(UTGame(WorldInfo.Game).DefaultInventory[index], false);
					}
				}
			}
		}
	}
}

// print error message on local players or on log
/** called when gameplay actually starts */
function MatchStarting()
{
	local int i;
	local string str, s;

	super.MatchStarting();

	if (ErrorMessages.Length > 0)
	{
		// init datastore for weapons and mutators
		DataCache = class'TestCWRUI'.static.GetData();

		for (i=0; i<ErrorMessages.Length; i++)
		{
			s = DataCache.DumpErrorInfo(ErrorMessages[i]);
			LogInternal(s);

			if (WorldInfo.NetMode != NM_DedicatedServer)
			{
				WriteToConsole(s);

				if (str != "" || i > 0) str $= Chr(10);
				str $= DataCache.GenerateErrorInfo(ErrorMessages[i]);
			}
		}

		if (WorldInfo.NetMode != NM_DedicatedServer)
		{
			DataCache.ShowErrorMessage(str, GetHumanReadableName());
		}

		// clear out data
		DataCache.Kill();
		DataCache = none;
	}
}

// Returns the human readable string representation of an object.
simulated function String GetHumanReadableName()
{
	return class'TestCWRUI'.static.GetMutatorName(class);
}

//**********************************************************************************
// Interface functions
//**********************************************************************************

function RegisterWeaponReplacement(Object Registrar, name OldClassName, string NewClassPath, bool bAmmo, ReplacementOptionsInfo ReplacementOptions)
{
	local int index;

	// ensure empty class path
	if (NewClassPath ~= "None") NewClassPath = "";
	NewClassPath = TrimRight(NewClassPath);

	if (bAmmo)
	{
		if (IsNewItem(AmmoToReplace, index, OldClassName))
		{
			AddWeaponReplacement(AmmoToReplace, Registrar, OldClassName, NewClassPath, ReplacementOptions);
		}
		else if (!(AmmoToReplace[index].NewClassPath ~= NewClassPath))
		{
			AddErrorMessage(AmmoToReplace[index].Registrar, Registrar, OldClassName, NewClassPath);
		}
	}
	else if (IsNewItem(WeaponsToReplace, index, OldClassName))
	{
		AddWeaponReplacement(WeaponsToReplace, Registrar, OldClassName, NewClassPath, ReplacementOptions);
	}
	else if (!(WeaponsToReplace[index].NewClassPath ~= NewClassPath))
	{
		AddErrorMessage(WeaponsToReplace[index].Registrar, Registrar, OldClassName, NewClassPath);
	}
}

function RegisterWeaponReplacementArray(Object Registrar, array<TemplateInfo> Replacements, bool bAmmo)
{
	local int i;
	for (i=0; i<Replacements.Length; i++)
	{
		RegisterWeaponReplacement(Registrar,
			Replacements[i].OldClassName,
			Replacements[i].NewClassPath,
			bAmmo,
			Replacements[i].Options);
	}
}

function UnRegisterWeaponReplacement(Object Registrar)
{
	local int i;

	for (i=0; i<WeaponsToReplace.Length; i++)
	{
		if (WeaponsToReplace[i].Registrar == Registrar)
		{
			WeaponsToReplace.Remove(i, 1);
			i--;
		}
	}

	for (i=0; i<AmmoToReplace.Length; i++)
	{
		if (AmmoToReplace[i].Registrar == Registrar)
		{
			AmmoToReplace.Remove(i, 1);
			i--;
		}
	}
}

static function bool StaticRegisterWeaponReplacement(Object Registrar, coerce name OldClassName, string NewClassPath, bool bAmmo, optional ReplacementOptionsInfo ReplacementOptions, optional bool bPre, optional bool bOnlyCheck, optional out string ErrorMessage, optional bool bSilent)
{
	local WorldInfo WI;
	local TestCentralWeaponReplacement mut;

	if (bPre)
	{
		return StaticPreRegisterWeaponReplacement(Registrar, OldClassName, NewClassPath, bAmmo, ReplacementOptions, bOnlyCheck, ErrorMessage, bSilent);
	}

	if (Registrar == none)
	{
		ErrorMessage = "No Registrar.";
		return false;
	}

	// check for WorldInfo
	if (!EnsureWorld(Registrar, WI))
	{
		ErrorMessage = "No WorldInfo found.";
		return false;
	}

	// spawn/create mutator
	EnsureMutator(WI, mut);

	// register the weapon replacement
	mut.RegisterWeaponReplacement(Registrar, OldClassName, NewClassPath, bAmmo, ReplacementOptions);
	return true;
}

static function bool StaticUnRegisterWeaponReplacement(Object Registrar, optional bool bPre, optional bool bSilent, optional out string ErrorMessage)
{
	local WorldInfo WI;
	local TestCentralWeaponReplacement mut;

	if (bPre)
	{
		return StaticPreUnRegisterWeaponReplacement(Registrar, bSilent, ErrorMessage);
	}

	if (Registrar == none)
	{
		ErrorMessage = "No Registrar.";
		return false;
	}

	// check for WorldInfo
	if (!EnsureWorld(Registrar, WI))
	{
		ErrorMessage = "No WorldInfo found.";
		return false;
	}

	// spawn/create mutator
	EnsureMutator(WI, mut);

	// register the weapon replacement
	mut.UnRegisterWeaponReplacement(Registrar);
	return true;
}

/** Create/Find the currently spawned mutator
 * @return true if newly created
 */
static function bool EnsureWorld(Object Registrar, out WorldInfo OutWorldInfo)
{
	if (Actor(Registrar) != none)
		OutWorldInfo = Actor(Registrar).WorldInfo;
	if (OutWorldInfo == none)
		OutWorldInfo = class'Engine'.static.GetCurrentWorldInfo();

	return OutWorldInfo != none;
}

/** Create/Find the currently spawned mutator
 * @return true if newly created
 */
static function bool EnsureMutator(WorldInfo WI, out TestCentralWeaponReplacement OutMut)
{
	// try to find an existent gneric mutator
	foreach WI.DynamicActors(class'TestCentralWeaponReplacement', OutMut)
		break;

	// if not mutator was found, create one
	if (OutMut == none)
	{
		OutMut = WI.Spawn(class'TestCentralWeaponReplacement', WI.Game);
		if (WI.Game != none)
		{
			OutMut.NextMutator = WI.Game.BaseMutator;
			WI.Game.BaseMutator = OutMut;
		}

		return true;
	}

	return false;
}

//**********************************************************************************
// Pre-Game/UI Interface functions
//**********************************************************************************

static function StaticPreInitialize()
{
	local TestCentralWeaponReplacement MutatorObj;
	local int i;

	default.StaticWeaponsToReplace.Length = 0;
	default.StaticAmmoToReplace.Length = 0;

	if (GetStaticMutator(MutatorObj))
	{
		MutatorObj.DataCache = class'TestCWRUI'.static.GetData();
	}

	for (i=0; i<default.DefaultWeaponsToReplace.Length; i++)
		StaticRegisterWeaponReplacement(none, default.DefaultWeaponsToReplace[i].OldClassName, default.DefaultWeaponsToReplace[i].NewClassPath, false, default.DefaultWeaponsToReplace[i].Options, true);

	for (i=0; i<default.DefaultAmmoToReplace.Length; i++)
		StaticRegisterWeaponReplacement(none, default.DefaultAmmoToReplace[i].OldClassName, default.DefaultAmmoToReplace[i].NewClassPath, true, default.DefaultAmmoToReplace[i].Options, true);
}

static function StaticPreDestroy()
{
	local TestCentralWeaponReplacement MutatorObj;

	default.StaticWeaponsToReplace.Length = 0;
	default.StaticAmmoToReplace.Length = 0;

	if (GetStaticMutator(MutatorObj) && MutatorObj.DataCache != none)
	{
		MutatorObj.DataCache.Kill();
		MutatorObj.DataCache = none;
	}
}

static function bool StaticPreRegisterWeaponReplacement(Object Registrar, name OldClassName, string NewClassPath, bool bAmmo, ReplacementOptionsInfo ReplacementOptions, optional bool bOnlyCheck, optional out string ErrorMessage, optional bool bBatchOp)
{
	local int index;

	// ensure empty class path
	if (NewClassPath ~= "None") NewClassPath = "";
	NewClassPath = TrimRight(NewClassPath);

	if (bAmmo)
	{
		if (IsNewItem(default.StaticAmmoToReplace, index, OldClassName))
		{
			if (!bOnlyCheck) AddWeaponReplacement(default.StaticAmmoToReplace, Registrar, OldClassName, NewClassPath, ReplacementOptions);
			return true;
		}
		else if (!(default.StaticAmmoToReplace[index].NewClassPath ~= NewClassPath))
		{
			ErrorMessage = class'TestCWRUI'.static.GetData().GetErrorMessage(default.StaticAmmoToReplace[index].Registrar, Registrar, OldClassName, NewClassPath);
		}
	}
	else if (IsNewItem(default.StaticWeaponsToReplace, index, OldClassName))
	{
		if (!bOnlyCheck) AddWeaponReplacement(default.StaticWeaponsToReplace, Registrar, OldClassName, NewClassPath, ReplacementOptions);
		return true;
	}
	else if (!(default.StaticWeaponsToReplace[index].NewClassPath ~= NewClassPath))
	{
		ErrorMessage = class'TestCWRUI'.static.GetData().GetErrorMessage(default.StaticWeaponsToReplace[index].Registrar, Registrar, OldClassName, NewClassPath);
	}

	return false;
}

static function bool StaticPreUnRegisterWeaponReplacement(Object Registrar, optional bool bSilent, optional out string ErrorMessage)
{
	local int i;
	local bool bAnyRemoved;

	if (Registrar == none)
	{
		ErrorMessage = "No Registrar.";
		return false;
	}

	for (i=0; i<default.StaticWeaponsToReplace.Length; i++)
	{
		if (default.StaticWeaponsToReplace[i].Registrar == Registrar)
		{
			bAnyRemoved = true;
			default.StaticWeaponsToReplace.Remove(i, 1);
			i--;
		}
	}

	for (i=0; i<default.StaticAmmoToReplace.Length; i++)
	{
		if (default.StaticAmmoToReplace[i].Registrar == Registrar)
		{
			bAnyRemoved = true;
			default.StaticAmmoToReplace.Remove(i, 1);
			i--;
		}
	}

	return bAnyRemoved;
}

//**********************************************************************************
// Private functions
//**********************************************************************************

static private function AddWeaponReplacement(out array<ReplacementInfoEx> arr, Object Registrar, name OldClassName, string NewClassPath, ReplacementOptionsInfo ReplacementOptions)
{
	local ReplacementInfoEx item;
	item.OldClassName = OldClassName;
	item.NewClassPath = NewClassPath;
	item.Registrar = Registrar;
	item.Options = ReplacementOptions;

	arr.AddItem(item);
}

private function AddErrorMessage(Object Conflicting, Object Registrar, name OldClassName, string NewClassPath)
{
	local int index;
	index = ErrorMessages.Length;
	ErrorMessages.Add(1);
	ErrorMessages[index].Conflict = Conflicting;
	ErrorMessages[index].Registrar = Registrar;
	ErrorMessages[index].ClassName = OldClassName;
	ErrorMessages[index].NewClassPath = NewClassPath;
}

function AddToLocker(class<UTWeapon> WeaponClass)
{
	local UTWeaponLocker Locker;
	local WeaponEntry ent;

	if (WeaponClass == none)
		return;

	foreach DynamicActors(class'UTWeaponLocker', Locker)
	{
		ent.WeaponClass = WeaponClass;
		Locker.MaxDesireability += WeaponClass.Default.AIRating;

		//if (WeaponClass.default.PickupFactoryMesh != none)
		//	ent.PickupMesh = WeaponClass.default.PickupFactoryMesh;

		Locker.Weapons.AddItem(ent);
	}
}

function bool ShouldBeReplaced(out int index, class ClassToCheck, bool bAmmo)
{
	local int i;
	if (bAmmo)
	{
		index = AmmoToReplace.Find('OldClassName', ClassToCheck.Name);
		if (index == INDEX_NONE)
		{
			for (i=0; i<AmmoToReplace.Length; i++)
			{
				if (AmmoToReplace[i].Options.bSubClasses && IsSubClass(ClassToCheck, AmmoToReplace[i].OldClassName))
				{
					index = i;
					return true;
				}
			}
		}
	}
	else
	{
		index = WeaponsToReplace.Find('OldClassName', ClassToCheck.Name);
		if (index == INDEX_NONE)
		{
			for (i=0; i<WeaponsToReplace.Length; i++)
			{
				if (WeaponsToReplace[i].Options.bSubClasses &&
					IsSubClass(ClassToCheck, WeaponsToReplace[i].OldClassName) &&
					!IgnoreSubClass(ClassToCheck, WeaponsToReplace[i].Options.IgnoreSubClasses))
				{
					index = i;
					return true;
				}
			}
		}
	}

	return index != INDEX_NONE;
}

//**********************************************************************************
// Helper functions
//**********************************************************************************

static function bool IsNewItem(out array<ReplacementInfoEx> items, out int index, name ClassName)
{
	index = items.Find('OldClassName', ClassName);
	return index == INDEX_NONE;
}

static function bool IsSubClass(class ClassToCheck, name ParentClassName)
{
	local Object Obj;
	// get default object which works for abstract classes
	return GetDefaultObject(ClassToCheck, obj) && obj.IsA(ParentClassName);
}

static function bool IgnoreSubClass(class ClassToCheck, array<ReplacementIgnoreClassesInfo> ParentCheck)
{
	local Object Obj;
	local int i;

	// get default object which works for abstract classes
	if (ParentCheck.Length > 0 && GetDefaultObject(ClassToCheck, obj))
	{
		for (i=0; i<ParentCheck.Length; i++)
		{
			if ((!ParentCheck[i].bSubClasses && ParentCheck[i].ClassName == ClassToCheck.Name) ||
				(ParentCheck[i].bSubClasses && obj.IsA(ParentCheck[i].ClassName)))
			{
				return true;
			}
		}
	}

	return false;
}

// NOTE: needed for hackfix
static function bool GetDefaultObject(class<Object> cls, out Object obj)
{
	local string path;

	path = GetDefaultPath(cls);
	obj = FindObject(path, cls);
	return (obj != none);
}

// NOTE: needed for hackfix
static function string GetDefaultPath(class<Object> cls)
{
	return cls.GetPackageName() $".Default__"$ cls.Name;
}

//**********************************************************************************
// Static functions
//**********************************************************************************

static function WriteToConsole(string message)
{
	local GameUISceneClient SceneClient;
	local Console con;

	SceneClient = class'UIRoot'.static.GetSceneClient();
	con = SceneClient.ViewportConsole;
	con.OutputText(message);
}

static function bool ClassPathValid(string classpath)
{
	if (classpath == "" || Len(classpath) > 63 || InStr(classpath, " ") != INDEX_NONE)
		return false;

	return (InStr(classpath, ".") != INDEX_NONE && Right(classpath, 1) != ".");
}

/** Trim the right part of the string */
static simulated function string TrimRight(coerce string sInput, optional coerce string sTrim = " ")
{
	local string sOutput;
	local int l;
	sOutput = sInput;

	l = Len(sTrim);
	while (Right(sOutput, l) == sTrim)
		sOutput = Left(sOutput, Len(sOutput)-1);

	return sOutput;
}

static function bool GetStaticMutator(out TestCentralWeaponReplacement OutMutator)
{
	local Object Obj;
	if (GetDefaultObject(default.Class, Obj) && TestCentralWeaponReplacement(Obj) != none)
	{
		OutMutator = TestCentralWeaponReplacement(Obj);
	}

	return OutMutator != none;
}

Defaultproperties
{
	GroupNames[0]="WEAPONMOD"

	ParameterProfile="CWRMapProfile"

	MessageErrors="Some errors occurred in replacing weapons:    `errors"
}

class TestCentralWeaponReplacement extends UTMutator
	dependson(TestCWRUI)
	config(TestCentralWeaponReplacement);

//**********************************************************************************
// Structs
//**********************************************************************************

struct ReplacementIgnoreClassesInfo
{
	/** class name of the pickup we want to ignore to be replaced */
	var name ClassName;
	/** Whether to check for subclasses  */
	var bool bSubClasses;

	structdefaultproperties
	{
		bSubClasses=true
	}
};

struct ReplacementOptionsInfo
{
	/** Whether to replace/remove the weapon */
	var bool bReplaceWeapon;
	/** Whether to check for subclasses  */
	var bool bSubClasses;
	/** Whether to add the weapon to the weapon lockers */
	var bool bAddToLocker;
	/** Whether to add the inventory item to the default inventory (on spawn) */
	var bool bAddToDefault;

	/** Whether to prevent replacing default inventory items (to be used with bSubClasses=true) */
	var bool bNoDefaultInventory<EditCondition=bSubClasses>;

	/** List of classes to ignore in checking for subclasses */
	var array<ReplacementIgnoreClassesInfo> IgnoreSubClasses<EditCondition=bSubClasses>;

	structdefaultproperties
	{
		bReplaceWeapon=true
		bSubClasses=false
		bAddToLocker=false
		bAddToDefault=false
		bNoDefaultInventory=false
	}
};

struct ReplacementInfoEx
{
	/** class name of the weapon we want to get rid of */
	var name OldClassName;
	/** fully qualified path of the class to replace it with */
	var string NewClassPath;

	/** the options for this item */
	var ReplacementOptionsInfo Options;

	/** the mutator/object which has registered the replacement
	(might be none if set by default configuration) */
	var transient Object Registrar;
};

struct TemplateInfo
{
	/** class name of the weapon we want to get rid of */
	var name OldClassName;
	/** fully qualified path of the class to replace it with */
	var string NewClassPath;

	/** the options for this item */
	var ReplacementOptionsInfo Options;

	/** Flag. Set whether to append the package name (of the current package) to the NewClassPath field */
	var bool AddPackage;
};

//**********************************************************************************
// Variables
//**********************************************************************************

var() const string ParameterProfile;

// Pre Game
// ------------

/** @ignore */
var private transient config array<ReplacementInfoEx> StaticWeaponsToReplace;
/** @ignore */
var private transient config array<ReplacementInfoEx> StaticAmmoToReplace;

// Config
// ------------

var config array<ReplacementInfoEx> DefaultWeaponsToReplace;
var config array<ReplacementInfoEx> DefaultAmmoToReplace;

// Workflow
// ------------

var array<ReplacementInfoEx> WeaponsToReplace;
var array<ReplacementInfoEx> AmmoToReplace;

var TestCWRUI DataCache;
var array<ErrorMessageInfo> ErrorMessages;

var array<int> EnforcerIndizes;

var string CurrentProfileName;
var TestCWRMapProfile CurrentProfile;

//**********************************************************************************
// Inherited functions
//**********************************************************************************

// Intialize the default replacments
event PreBeginPlay()
{
	local int i;
	super.PreBeginPlay();

	if (!IsPendingKill())
	{
		WeaponsToReplace.Length = 0;
		AmmoToReplace.Length = 0;

		for (i=0; i<DefaultWeaponsToReplace.Length; i++)
			RegisterWeaponReplacement(none, DefaultWeaponsToReplace[i].OldClassName, DefaultWeaponsToReplace[i].NewClassPath, false, DefaultWeaponsToReplace[i].Options);

		for (i=0; i<DefaultAmmoToReplace.Length; i++)
			RegisterWeaponReplacement(none, DefaultAmmoToReplace[i].OldClassName, DefaultAmmoToReplace[i].NewClassPath, true, DefaultAmmoToReplace[i].Options);
	}
}

// used to check for default inventory items to be replaced or
// whether an item should be added to the weapon lockers or the default inventory
function InitMutator(string Options, out string ErrorMessage)
{
	local int i, j;
	local UTGame G;
	local class<UTWeapon> WeaponClass;
	local TestCWRMapProfile MapProvider;

	super.InitMutator(Options, ErrorMessage);

	if (class'GameInfo'.static.HasOption(Options, ParameterProfile))
	{
		CurrentProfileName = class'GameInfo'.static.ParseOption(Options, ParameterProfile);
	}

	if (CurrentProfileName != "" && class'TestCWRMapProfile'.static.GetMapProfileByName(CurrentProfileName, MapProvider))
	{
		CurrentProfile = MapProvider;
		RegisterWeaponReplacementArray(CurrentProfile, CurrentProfile.WeaponsToReplace, false);
		RegisterWeaponReplacementArray(CurrentProfile, CurrentProfile.AmmoToReplace, true);
	}

	// Make sure the game does not hold a null reference
	G = UTGame(WorldInfo.Game);
	if(G != none)
	{
		for (j=0; j<WeaponsToReplace.Length; j++)
		{
			if (WeaponsToReplace[j].Options.bReplaceWeapon && !WeaponsToReplace[j].Options.bNoDefaultInventory)
			{
				for (i=0; i<G.DefaultInventory.length; i++)
				{
					if (G.DefaultInventory[i] == None) continue;
					if (!WeaponsToReplace[j].Options.bSubClasses && G.DefaultInventory[i].Name != WeaponsToReplace[j].OldClassName) continue;

					// IsA doesn't work for abstract classes, we need to use the workaround/hackfix
					if (WeaponsToReplace[j].Options.bSubClasses && !IsSubClass(G.DefaultInventory[i], WeaponsToReplace[j].OldClassName)) continue;

					if (!ClassPathValid(WeaponsToReplace[j].NewClassPath))
					{
						// replace with nothing
						G.DefaultInventory.Remove(i, 1);
						i--;
						continue;
					}
					else
					{
						WeaponClass = class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[j].NewClassPath, class'Class'));
						if (WeaponsToReplace[j].Options.bReplaceWeapon)
						{
							G.DefaultInventory[i] = WeaponClass;
						}
					}
				}
			}

			if (WeaponsToReplace[j].Options.bAddToDefault && ClassPathValid(WeaponsToReplace[j].NewClassPath))
			{
				WeaponClass = class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[j].NewClassPath, class'Class'));
				if (WeaponClass != none) G.DefaultInventory.AddItem(WeaponClass);
			}

			if (WeaponsToReplace[j].Options.bAddToLocker)
			{
				WeaponClass = class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[j].NewClassPath, class'Class'));
				AddToLocker(WeaponClass);
			}
		}

		if (G.TranslocatorClass != None)
		{
			j = WeaponsToReplace.Find('OldClassName', G.TranslocatorClass.Name);
			if (j != INDEX_NONE)
			{
				if (WeaponsToReplace[j].NewClassPath == "" || !WeaponsToReplace[j].Options.bReplaceWeapon)
				{
					// replace with nothing
					G.TranslocatorClass = None;
				}
				else
				{
					G.TranslocatorClass = class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[j].NewClassPath, class'Class'));
				}
			}
		}

		EnforcerIndizes.Length = 0;
		for (i=0; i<G.DefaultInventory.length; i++)
		{
			if (IsSubClass(G.DefaultInventory[i], 'UTWeap_Enforcer'))
			{
				EnforcerIndizes.AddItem(i);
			}
		}
	}
}

// check for factory and replace/remove their inventory item (if desired)
function bool CheckReplacement(Actor Other)
{
	local UTWeaponPickupFactory WeaponPickup;
	local UTWeaponLocker Locker;
	local UTAmmoPickupFactory AmmoPickup, NewAmmo;
	local int i, Index;
	local class<UTAmmoPickupFactory> NewAmmoClass;

	WeaponPickup = UTWeaponPickupFactory(Other);
	if (WeaponPickup != None)
	{
		if (WeaponPickup.WeaponPickupClass != None)
		{
			if (ShouldBeReplaced(index, WeaponPickup.WeaponPickupClass, false) && WeaponsToReplace[index].Options.bReplaceWeapon)
			{
				if (WeaponsToReplace[index].NewClassPath == "")
				{
					// replace with nothing
					return false;
				}
				WeaponPickup.WeaponPickupClass = class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[index].NewClassPath, class'Class'));
				WeaponPickup.InitializePickup();
			}
		}
	}
	else
	{
		if (UTWeaponLocker(Other) != None)
		{
			Locker = UTWeaponLocker(Other);
			for (i = 0; i < Locker.Weapons.length; i++)
			{
				if (Locker.Weapons[i].WeaponClass != none && ShouldBeReplaced(index, Locker.Weapons[i].WeaponClass, false) && WeaponsToReplace[index].Options.bReplaceWeapon)
				{
					if (WeaponsToReplace[index].NewClassPath == "")
					{
						// replace with nothing
						Locker.ReplaceWeapon(i, None);
					}
					else
					{
						Locker.ReplaceWeapon(i, class<UTWeapon>(DynamicLoadObject(WeaponsToReplace[index].NewClassPath, class'Class')));
					}
				}
			}
		}
		else if (UTAmmoPickupFactory(Other) != none)
		{
			AmmoPickup = UTAmmoPickupFactory(Other);
			if (AmmoPickup.Class != none)
			{
				if (ShouldBeReplaced(index, AmmoPickup.Class, true) && AmmoToReplace[index].Options.bReplaceWeapon)
				{
					if (AmmoToReplace[index].NewClassPath == "")
					{
						// replace with nothing
						return false;
					}
					NewAmmoClass = class<UTAmmoPickupFactory>(DynamicLoadObject(AmmoToReplace[index].NewClassPath, class'Class'));
					if (NewAmmoClass == None)
					{
						// replace with nothing
						return false;
					}
					else if (NewAmmoClass.default.bStatic || NewAmmoClass.default.bNoDelete)
					{
						// transform the current ammo into the desired class
						AmmoPickup.TransformAmmoType(NewAmmoClass);
						return true;
					}
					else
					{
						// spawn the new ammo, link it to the old, then disable the old one
						NewAmmo = AmmoPickup.Spawn(NewAmmoClass);
						NewAmmo.OriginalFactory = AmmoPickup;
						AmmoPickup.ReplacementFactory = NewAmmo;
						return false;
					}
				}
			}
		}

		// remove initial anim for Enforcers (may only work on server/listen player)
		else if (EnforcerIndizes.Length > 1 && UTWeap_Enforcer(Other) != none)
		{
			UTWeap_Enforcer(Other).bLoaded = true;
			UTWeap_Enforcer(Other).EquipTime = UTWeap_Enforcer(Other).ReloadTime;
		}
	}

	return true;
}

// Fix Dual Enforcer
/** called by GameInfo.RestartPlayer() */
function ModifyPlayer(Pawn Other)
{
	local int i, index;
	local UTWeap_Enforcer Enf;
	super.ModifyPlayer(Other);

	// special case for Akimbo (Enforcer only)
	if (EnforcerIndizes.Length > 1 && UTGame(WorldInfo.Game) != none)
	{
		index = EnforcerIndizes[0];
		Enf = UTWeap_Enforcer(Other.FindInventoryType(UTGame(WorldInfo.Game).DefaultInventory[0]));
		if (Enf != none)
		{
			for (i=1; i<EnforcerIndizes.Length; i++)
			{
				index = EnforcerIndizes[i];
				if (index < UTGame(WorldInfo.Game).DefaultInventory.Length)
				{
					if (!Enf.DenyPickupQuery(UTGame(WorldInfo.Game).DefaultInventory[index], none))
					{
						Other.CreateInventory(UTGame(WorldInfo.Game).DefaultInventory[index], false);
					}
				}
			}
		}
	}
}

// print error message on local players or on log
/** called when gameplay actually starts */
function MatchStarting()
{
	local int i;
	local string str, s;

	super.MatchStarting();

	if (ErrorMessages.Length > 0)
	{
		// init datastore for weapons and mutators
		DataCache = class'TestCWRUI'.static.GetData();

		for (i=0; i<ErrorMessages.Length; i++)
		{
			s = DataCache.DumpErrorInfo(ErrorMessages[i]);
			LogInternal(s);

			if (WorldInfo.NetMode != NM_DedicatedServer)
			{
				WriteToConsole(s);

				if (str != "" || i > 0) str $= Chr(10);
				str $= DataCache.GenerateErrorInfo(ErrorMessages[i]);
			}
		}

		if (WorldInfo.NetMode != NM_DedicatedServer)
		{
			DataCache.ShowErrorMessage(str, GetHumanReadableName());
		}

		// clear out data
		DataCache.Kill();
		DataCache = none;
	}
}

// Returns the human readable string representation of an object.
simulated function String GetHumanReadableName()
{
	return class'TestCWRUI'.static.GetMutatorName(class);
}

//**********************************************************************************
// Interface functions
//**********************************************************************************

function RegisterWeaponReplacement(Object Registrar, name OldClassName, string NewClassPath, bool bAmmo, ReplacementOptionsInfo ReplacementOptions)
{
	local int index;

	// ensure empty class path
	if (NewClassPath ~= "None") NewClassPath = "";
	NewClassPath = TrimRight(NewClassPath);

	if (bAmmo)
	{
		if (IsNewItem(AmmoToReplace, index, OldClassName))
		{
			AddWeaponReplacement(AmmoToReplace, Registrar, OldClassName, NewClassPath, ReplacementOptions);
		}
		else if (!(AmmoToReplace[index].NewClassPath ~= NewClassPath))
		{
			AddErrorMessage(AmmoToReplace[index].Registrar, Registrar, OldClassName, NewClassPath);
		}
	}
	else if (IsNewItem(WeaponsToReplace, index, OldClassName))
	{
		AddWeaponReplacement(WeaponsToReplace, Registrar, OldClassName, NewClassPath, ReplacementOptions);
	}
	else if (!(WeaponsToReplace[index].NewClassPath ~= NewClassPath))
	{
		AddErrorMessage(WeaponsToReplace[index].Registrar, Registrar, OldClassName, NewClassPath);
	}
}

function RegisterWeaponReplacementArray(Object Registrar, array<TemplateInfo> Replacements, bool bAmmo)
{
	local int i;
	for (i=0; i<Replacements.Length; i++)
	{
		RegisterWeaponReplacement(Registrar,
			Replacements[i].OldClassName,
			Replacements[i].NewClassPath,
			bAmmo,
			Replacements[i].Options);
	}
}

function UnRegisterWeaponReplacement(Object Registrar)
{
	local int i;

	for (i=0; i<WeaponsToReplace.Length; i++)
	{
		if (WeaponsToReplace[i].Registrar == Registrar)
		{
			WeaponsToReplace.Remove(i, 1);
			i--;
		}
	}

	for (i=0; i<AmmoToReplace.Length; i++)
	{
		if (AmmoToReplace[i].Registrar == Registrar)
		{
			AmmoToReplace.Remove(i, 1);
			i--;
		}
	}
}

static function bool StaticRegisterWeaponReplacement(Object Registrar, coerce name OldClassName, string NewClassPath, bool bAmmo, optional ReplacementOptionsInfo ReplacementOptions, optional bool bPre, optional bool bOnlyCheck, optional out string ErrorMessage, optional bool bSilent)
{
	local WorldInfo WI;
	local TestCentralWeaponReplacement mut;

	if (bPre)
	{
		return StaticPreRegisterWeaponReplacement(Registrar, OldClassName, NewClassPath, bAmmo, ReplacementOptions, bOnlyCheck, ErrorMessage, bSilent);
	}

	if (Registrar == none)
	{
		ErrorMessage = "No Registrar.";
		return false;
	}

	// check for WorldInfo
	if (!EnsureWorld(Registrar, WI))
	{
		ErrorMessage = "No WorldInfo found.";
		return false;
	}

	// spawn/create mutator
	EnsureMutator(WI, mut);

	// register the weapon replacement
	mut.RegisterWeaponReplacement(Registrar, OldClassName, NewClassPath, bAmmo, ReplacementOptions);
	return true;
}

static function bool StaticUnRegisterWeaponReplacement(Object Registrar, optional bool bPre, optional bool bSilent, optional out string ErrorMessage)
{
	local WorldInfo WI;
	local TestCentralWeaponReplacement mut;

	if (bPre)
	{
		return StaticPreUnRegisterWeaponReplacement(Registrar, bSilent, ErrorMessage);
	}

	if (Registrar == none)
	{
		ErrorMessage = "No Registrar.";
		return false;
	}

	// check for WorldInfo
	if (!EnsureWorld(Registrar, WI))
	{
		ErrorMessage = "No WorldInfo found.";
		return false;
	}

	// spawn/create mutator
	EnsureMutator(WI, mut);

	// register the weapon replacement
	mut.UnRegisterWeaponReplacement(Registrar);
}

/** Create/Find the currently spawned mutator
 * @return true if newly created
 */
static function bool EnsureWorld(Object Registrar, out WorldInfo OutWorldInfo)
{
	if (Actor(Registrar) != none)
		OutWorldInfo = Actor(Registrar).WorldInfo;
	if (OutWorldInfo == none)
		OutWorldInfo = class'Engine'.static.GetCurrentWorldInfo();

	return OutWorldInfo != none;
}

/** Create/Find the currently spawned mutator
 * @return true if newly created
 */
static function bool EnsureMutator(WorldInfo WI, out TestCentralWeaponReplacement OutMut)
{
	// try to find an existent gneric mutator
	foreach WI.DynamicActors(class'TestCentralWeaponReplacement', OutMut)
		break;

	// if not mutator was found, create one
	if (OutMut == none)
	{
		OutMut = WI.Spawn(class'TestCentralWeaponReplacement', WI.Game);
		if (WI.Game != none)
		{
			OutMut.NextMutator = WI.Game.BaseMutator;
			WI.Game.BaseMutator = OutMut;
		}

		return true;
	}

	return false;
}

//**********************************************************************************
// Pre-Game/UI Interface functions
//**********************************************************************************

static function StaticPreInitialize()
{
	local TestCentralWeaponReplacement MutatorObj;

	default.StaticWeaponsToReplace.Length = 0;
	default.StaticAmmoToReplace.Length = 0;

	if (GetStaticMutator(MutatorObj))
	{
		MutatorObj.DataCache = class'TestCWRUI'.static.GetData();
	}
}

static function StaticPreDestroy()
{
	local TestCentralWeaponReplacement MutatorObj;

	default.StaticWeaponsToReplace.Length = 0;
	default.StaticAmmoToReplace.Length = 0;

	if (GetStaticMutator(MutatorObj) && MutatorObj.DataCache != none)
	{
		MutatorObj.DataCache.Kill();
		MutatorObj.DataCache = none;
	}
}

static function bool StaticPreRegisterWeaponReplacement(Object Registrar, name OldClassName, string NewClassPath, bool bAmmo, ReplacementOptionsInfo ReplacementOptions, optional bool bOnlyCheck, optional out string ErrorMessage, optional bool bBatchOp)
{
	local int index;

	// ensure empty class path
	if (NewClassPath ~= "None") NewClassPath = "";
	NewClassPath = TrimRight(NewClassPath);

	if (bAmmo)
	{
		if (IsNewItem(default.StaticAmmoToReplace, index, OldClassName))
		{
			if (!bOnlyCheck) AddWeaponReplacement(default.StaticAmmoToReplace, Registrar, OldClassName, NewClassPath, ReplacementOptions);
			return true;
		}
		else if (!(default.StaticAmmoToReplace[index].NewClassPath ~= NewClassPath))
		{
			ErrorMessage = class'TestCWRUI'.static.GetData().GetErrorMessage(default.StaticAmmoToReplace[index].Registrar, Registrar, OldClassName, NewClassPath);
		}
	}
	else if (IsNewItem(default.StaticWeaponsToReplace, index, OldClassName))
	{
		if (!bOnlyCheck) AddWeaponReplacement(default.StaticWeaponsToReplace, Registrar, OldClassName, NewClassPath, ReplacementOptions);
		return true;
	}
	else if (!(default.StaticWeaponsToReplace[index].NewClassPath ~= NewClassPath))
	{
		ErrorMessage = class'TestCWRUI'.static.GetData().GetErrorMessage(default.StaticWeaponsToReplace[index].Registrar, Registrar, OldClassName, NewClassPath);
	}

	return false;
}

static function bool StaticPreUnRegisterWeaponReplacement(Object Registrar, optional bool bSilent, optional out string ErrorMessage)
{
	local int i;
	local bool bAnyRemoved;

	if (Registrar == none)
	{
		ErrorMessage = "No Registrar.";
		return false;
	}

	for (i=0; i<default.StaticWeaponsToReplace.Length; i++)
	{
		if (default.StaticWeaponsToReplace[i].Registrar == Registrar)
		{
			bAnyRemoved = true;
			default.StaticWeaponsToReplace.Remove(i, 1);
			i--;
		}
	}

	for (i=0; i<default.StaticAmmoToReplace.Length; i++)
	{
		if (default.StaticAmmoToReplace[i].Registrar == Registrar)
		{
			bAnyRemoved = true;
			default.StaticAmmoToReplace.Remove(i, 1);
			i--;
		}
	}

	return bAnyRemoved;
}

//**********************************************************************************
// Private functions
//**********************************************************************************

static private function AddWeaponReplacement(out array<ReplacementInfoEx> arr, Object Registrar, name OldClassName, string NewClassPath, ReplacementOptionsInfo ReplacementOptions)
{
	local ReplacementInfoEx item;
	item.OldClassName = OldClassName;
	item.NewClassPath = NewClassPath;
	item.Registrar = Registrar;
	item.Options = ReplacementOptions;

	arr.AddItem(item);
}

private function AddErrorMessage(Object Conflicting, Object Registrar, name OldClassName, string NewClassPath)
{
	local int index;
	index = ErrorMessages.Length;
	ErrorMessages.Add(1);
	ErrorMessages[index].Conflict = Conflicting;
	ErrorMessages[index].Registrar = Registrar;
	ErrorMessages[index].ClassName = OldClassName;
	ErrorMessages[index].NewClassPath = NewClassPath;
}

function AddToLocker(class<UTWeapon> WeaponClass)
{
	local UTWeaponLocker Locker;
	local WeaponEntry ent;

	if (WeaponClass == none)
		return;

	foreach DynamicActors(class'UTWeaponLocker', Locker)
	{
		ent.WeaponClass = WeaponClass;
		Locker.MaxDesireability += WeaponClass.Default.AIRating;

		//if (WeaponClass.default.PickupFactoryMesh != none)
		//	ent.PickupMesh = WeaponClass.default.PickupFactoryMesh;

		Locker.Weapons.AddItem(ent);
	}
}

function bool ShouldBeReplaced(out int index, class ClassToCheck, bool bAmmo)
{
	local int i;
	if (bAmmo)
	{
		index = AmmoToReplace.Find('OldClassName', ClassToCheck.Name);
		if (index == INDEX_NONE)
		{
			for (i=0; i<AmmoToReplace.Length; i++)
			{
				if (AmmoToReplace[i].Options.bSubClasses && IsSubClass(ClassToCheck, AmmoToReplace[i].OldClassName))
				{
					index = i;
					return true;
				}
			}
		}
	}
	else
	{
		index = WeaponsToReplace.Find('OldClassName', ClassToCheck.Name);
		if (index == INDEX_NONE)
		{
			for (i=0; i<WeaponsToReplace.Length; i++)
			{
				if (WeaponsToReplace[i].Options.bSubClasses &&
					IsSubClass(ClassToCheck, WeaponsToReplace[i].OldClassName) &&
					!IgnoreSubClass(ClassToCheck, WeaponsToReplace[i].Options.IgnoreSubClasses))
				{
					index = i;
					return true;
				}
			}
		}
	}

	return index != INDEX_NONE;
}

//**********************************************************************************
// Helper functions
//**********************************************************************************

static function bool IsNewItem(out array<ReplacementInfoEx> items, out int index, name ClassName)
{
	index = items.Find('OldClassName', ClassName);
	return index == INDEX_NONE;
}

static function bool IsSubClass(class ClassToCheck, name ParentClassName)
{
	local Object Obj;
	// get default object which works for abstract classes
	return GetDefaultObject(ClassToCheck, obj) && obj.IsA(ParentClassName);
}

static function bool IgnoreSubClass(class ClassToCheck, array<ReplacementIgnoreClassesInfo> ParentCheck)
{
	local Object Obj;
	local int i;

	// get default object which works for abstract classes
	if (ParentCheck.Length > 0 && GetDefaultObject(ClassToCheck, obj))
	{
		for (i=0; i<ParentCheck.Length; i++)
		{
			if ((!ParentCheck[i].bSubClasses && ParentCheck[i].ClassName == ClassToCheck.Name) ||
				(ParentCheck[i].bSubClasses && obj.IsA(ParentCheck[i].ClassName)))
			{
				return true;
			}
		}
	}

	return false;
}

// NOTE: needed for hackfix
static function bool GetDefaultObject(class<Object> cls, out Object obj)
{
	local string path;

	path = GetDefaultPath(cls);
	obj = FindObject(path, cls);
	return (obj != none);
}

// NOTE: needed for hackfix
static function string GetDefaultPath(class<Object> cls)
{
	return cls.GetPackageName() $".Default__"$ cls.Name;
}

//**********************************************************************************
// Static functions
//**********************************************************************************

static function WriteToConsole(string message)
{
	local GameUISceneClient SceneClient;
	local Console con;

	SceneClient = class'UIRoot'.static.GetSceneClient();
	con = SceneClient.ViewportConsole;
	con.OutputText(message);
}

static function bool ClassPathValid(string classpath)
{
	if (classpath == "" || Len(classpath) > 63 || InStr(classpath, " ") != INDEX_NONE)
		return false;

	return (InStr(classpath, ".") != INDEX_NONE && Right(classpath, 1) != ".");
}

/** Trim the right part of the string */
static simulated function string TrimRight(coerce string sInput, optional coerce string sTrim = " ")
{
	local string sOutput;
	local int l;
	sOutput = sInput;

	l = Len(sTrim);
	while (Right(sOutput, l) == sTrim)
		sOutput = Left(sOutput, Len(sOutput)-1);

	return sOutput;
}

static function bool GetStaticMutator(out TestCentralWeaponReplacement OutMutator)
{
	local Object Obj;
	if (GetDefaultObject(default.Class, Obj) && TestCentralWeaponReplacement(Obj) != none)
	{
		OutMutator = TestCentralWeaponReplacement(Obj);
	}

	return OutMutator != none;
}

Defaultproperties
{
	GroupNames[0]="WEAPONMOD"

	ParameterProfile="CWRMapProfile"

	MessageErrors="Some errors occurred in replacing weapons:    `errors"
}