Item trade mod

From Heroes of Hammerwatch wiki
Jump to: navigation, search

This is not really a step by step tutorial, more like a "How it's made" for people who are interested. I recommend using something like sublime to search through all the game files and https://docs.heroesofhammerwatch.com/ to be able to find some things we might not be able to find with the search of sublime.

So thinking of the Item trade mod: In this case I think we can partly copy the item gambler, since it displays your own items and you're able to select them. Which is what we need so we know which items we want to send.

equipped items, drinks and other things that can be different for each character are usually stored inside the playerrecord, but since we probably can copy a big part of the item gamblers logic we don't need to build that ourselves anymore.

I copy a small part from Miss's trainer mod, to be able to open a UI with a F button, in this case I set it to F7. F7 equals to KeyState number 64 according to the documentation: https://docs.heroesofhammerwatch.com/engine/group___input.html#enum-members

And we add the code:

namespace ItemTradeHook
{
	ItemTrade@ g_interface;

	[Hook]
	void GameModeUpdate(Campaign@ campaign, int dt, GameInput& gameInput, MenuInput& menuInput)
	{
		if (g_interface is null)
			return;

		if (Platform::GetKeyState(64).Pressed) // F7
			campaign.ToggleUserWindow(g_interface);
		}
	}

	[Hook]
	void GameModeStart(Campaign@ campaign, SValue@ save)
	{
		campaign.m_userWindows.insertLast(@g_interface = ItemTrade(campaign.m_guiBuilder));
	}
}

We put it inside the GameModeUpdate hook, so the game can check every tick if we pressed the button or not, overall hooks are better to use than overwriting files. We initialize our g_interface variable and inside the GameModeStart hook we add our own UserWindow type class. (All hooks can be found on the wiki, or type ".wiki hooks" without quotes of course, in the hoh discord. Preferably in the modding channel to get the link)


Now we have a F button to open our window, we haven't added a UI yet so we can't open anything. So first we initialize our Class UI. If we lookup 'ToggleUserWindow' in the docs, we see it expects a type of 'UserWindow'. This basically means our class type needs to be a UserWindow, alright, let's look for the itemgambler.


Looking through the game files I couldn't find a 'itemgambler' file so I went to the docs, searching for itemgambler returned nothing, Maybe if we search item on the docs it will bring up something interesting. Looking through all the hits there are a few that might be what we're looking for: ItemDealer class, itemDealerSaved variable inside PlayerRecord, and itemDealerReward variable inside PlayerRecord.


Let's take a look inside ItemDealer class, it seems like this is what we're looking but this isn't a UserWindow, it inherits from ScriptWidgetHost.

One thing we can do is trying to convert it to a UserWindow, the UserWindow and ScriptWidgetHost inherits from IWidgetHoster so they have some similarities. (I don't know if this is the correct way to do it, but I did it like this.) We copy the whole content of ItemDealer into our own class, I called mine ItemTrade.


UserWindow doesn't have a Initialize function like ScriptWidgetHost so we copy the contents of this function and place it in the constructor. Also, UserWindow expects a GUIBuilder in the parameter so we change that, and we call the super() with our gui file. We end up with:

ItemTrade(GUIBuilder@ b)
{
	super(b, "gui/itemtrade/thyraxxitemtrade.gui");

	@m_wItemList = m_widget.GetWidgetById("list-items");
	@m_wItemTemplate = m_widget.GetWidgetById("template-item");

	@m_wQualityList = m_widget.GetWidgetById("list-qualities");
	@m_wQualityTemplate = m_widget.GetWidgetById("template-quality");

	@m_wGamble = cast<ScalableSpriteButtonWidget>(m_widget.GetWidgetById("gamble"));
	@m_wFinalItem = cast<RectWidget>(m_widget.GetWidgetById("final-item"));

	auto gm = cast<Campaign>(g_gameMode);
	auto record = GetLocalPlayerRecord();

	if (record.itemDealerSaved == gm.m_levelCount + 1)
	{
		auto finalItem = g_items.GetItem(record.itemDealerReward);
		if (finalItem !is null)
		{
			ReloadItemList(false);
			ShowFinalItem(finalItem);
		}
		else
		{
			//???
		        ReloadItemList();
		}
	}else
	        ReloadItemList();

	UpdateButton();

	//PauseGame(true, true); //Keep it like this or remove this line.

}

You can remove the pausegame, online pause is not possible as far as I know, also after a bit of testing it seems the game will completely pause on start, I removed it. (Duh, it's initialized on startup) Secondly, further at the bottom of our copied file contents there is also a function named 'Stop()', this function overrides a base function from ScriptWidgetHost, since we don't use ScriptWidgetHost, completely remove this function.

And lastly, inside the OnFunc function, the function 'Stop()' is called, since this was for ScriptWidgetHost we need to change this. To close the window when pressing the 'X' on the window we can use the Close() function from UserWindow, so change Stop() to Close().

Now your file should look like this:

class ItemTrade : UserWindow
{
	Widget@ m_wItemList;
	Widget@ m_wItemTemplate;

	Widget@ m_wQualityList;
	Widget@ m_wQualityTemplate;

	ScalableSpriteButtonWidget@ m_wGamble;
	RectWidget@ m_wFinalItem;

	ItemTrade(GUIBuilder@ b)
	{
		super(b, "gui/itemtrade/thyraxxitemtrade.gui");

		@m_wItemList = m_widget.GetWidgetById("list-items");
		@m_wItemTemplate = m_widget.GetWidgetById("template-item");

		@m_wQualityList = m_widget.GetWidgetById("list-qualities");
		@m_wQualityTemplate = m_widget.GetWidgetById("template-quality");

		@m_wGamble = cast<ScalableSpriteButtonWidget>(m_widget.GetWidgetById("gamble"));
		@m_wFinalItem = cast<RectWidget>(m_widget.GetWidgetById("final-item"));

		auto gm = cast<Campaign>(g_gameMode);
		auto record = GetLocalPlayerRecord();

		if (record.itemDealerSaved == gm.m_levelCount + 1)
		{
			auto finalItem = g_items.GetItem(record.itemDealerReward);
			if (finalItem !is null)
			{
				ReloadItemList(false);
				ShowFinalItem(finalItem);
			}
			else
			{
				//???
				ReloadItemList();
			}
		}
		else
			ReloadItemList();

		UpdateButton();
	}

	void UpdateButton()
	{
		int numSelected = 0;
		for (uint i = 0; i < m_wItemList.m_children.length(); i++)
		{
			auto wItem = cast<CheckBoxWidget>(m_wItemList.m_children[i]);
			if (wItem !is null && wItem.IsChecked())
				numSelected++;
		}

		m_wGamble.m_enabled = (numSelected > 0);
		m_wGamble.m_tooltipTitle = Resources::GetString(".itemdealer.tooltip.title");
		if (numSelected == 1)
			m_wGamble.m_tooltipText = Resources::GetString(".itemdealer.tooltip.single");
		else
			m_wGamble.m_tooltipText = Resources::GetString(".itemdealer.tooltip.plural", { { "num", numSelected } });

		int points = GetCurrentPoints();
		auto svReward = GetReward(points);

		array<ActorItemQuality> qualities;

		if (svReward !is null)
		{
			auto arrQualities = GetParamArray(UnitPtr(), svReward, "qualities");
			for (uint i = 0; i < arrQualities.length(); i++)
				qualities.insertLast(ParseActorItemQuality(arrQualities[i].GetString()));
		}

		m_wQualityList.ClearChildren();

		for (int i = 1; i < 6; i++)
		{
			auto quality = ActorItemQuality(i);
			string qualityName = GetItemQualityName(quality);
			vec4 qualityColor = GetItemQualityColor(quality);

			int num = 0;
			float ratio = 0.0f;

			for (uint j = 0; j < qualities.length(); j++)
			{
				if (qualities[j] == quality)
					num++;
			}

			if (qualities.length() > 0)
				ratio = num / float(qualities.length());

			auto wNewQuality = m_wQualityTemplate.Clone();
			wNewQuality.SetID("");
			wNewQuality.m_visible = true;

			wNewQuality.m_tooltipTitle = "\\c" + GetItemQualityColorString(quality) + Resources::GetString(".quality." + qualityName);

			auto wQuality = cast<SpriteWidget>(wNewQuality.GetWidgetById("quality"));
			if (wQuality !is null)
				wQuality.SetSprite("quality-" + qualityName);

			auto wPercentage = cast<TextWidget>(wNewQuality.GetWidgetById("percentage"));
			if (wPercentage !is null)
			{
				wPercentage.m_color = qualityColor;
				wPercentage.SetText(ceil(ratio * 100.0f) + "%");
			}

			m_wQualityList.AddChild(wNewQuality);
		}
	}

	void ShowFinalItem(ActorItem@ item)
	{
		auto record = GetLocalPlayerRecord();

		SetActorItemOnGui(record, m_wFinalItem, item);

		m_wFinalItem.m_color = GetItemQualityBackgroundColor(item.quality);

		m_wGamble.m_enabled = false;
		m_wGamble.m_tooltipTitle = "";
		m_wGamble.m_tooltipText = "";
	}

	void ReloadItemList(bool enabled = true)
	{
		m_wItemList.ClearChildren();

		auto record = GetLocalPlayerRecord();
		for (uint i = 0; i < record.items.length(); i++)
		{
			auto item = g_items.GetItem(record.items[i]);
			if (item is null)
			{
				PrintError("Couldn't find item at index " + i);
				continue;
			}

			auto wNewItem = cast<CheckBoxWidget>(AddActorItemToGuiList(record, m_wItemList, m_wItemTemplate, item));
			if (wNewItem !is null)
				wNewItem.m_enabled = enabled;
		}

		m_wGamble.m_enabled = false;
	}

	int GetCurrentPoints()
	{
		auto player = GetLocalPlayer();

		int points = 0;
		for (uint i = 0; i < m_wItemList.m_children.length(); i++)
		{
			auto wItem = cast<CheckBoxWidget>(m_wItemList.m_children[i]);
			if (wItem is null || !wItem.IsChecked())
				continue;

			auto item = g_items.GetItem(wItem.m_value);
			if (item is null)
				continue;

			switch (item.quality)
			{
				case ActorItemQuality::Common: points += 1; break;
				case ActorItemQuality::Uncommon: points += 3; break;
				case ActorItemQuality::Rare: points += 7; break;
				case ActorItemQuality::Epic: points += 12; break;
				case ActorItemQuality::Legendary: points += 24; break;
			}
		}
		return points;
	}

	SValue@ GetReward(int points)
	{
		auto svalRewards = Resources::GetSValue("tweak/itemdealer.sval");
		auto arrRewards = svalRewards.GetArray();
		for (uint i = 0; i < arrRewards.length(); i++)
		{
			auto sv = arrRewards[i];

			int minPoints = GetParamInt(UnitPtr(), sv, "min-points", false);
			if (points > minPoints)
				return sv;
		}
		return null;
	}

	void CommitGamble()
	{
		auto gm = cast<Campaign>(g_gameMode);

		auto player = GetLocalPlayer();
		if (player is null)
		{
			PrintError("Player is dead!");
			return;
		}

		int points = GetCurrentPoints();

		int numItemsLost = 0;
		for (uint i = 0; i < m_wItemList.m_children.length(); i++)
		{
			auto wItem = cast<CheckBoxWidget>(m_wItemList.m_children[i]);
			if (wItem is null || !wItem.IsChecked())
				continue;

			auto item = g_items.GetItem(wItem.m_value);
			if (item is null)
				continue;

			player.TakeItem(item);
			player.m_record.itemsRecycled.insertLast(item.id);
			numItemsLost++;
		}

		Platform::Service.UnlockAchievement("item_dealer_used");

		Stats::Add("item-dealer", 1, player.m_record);
		Stats::Add("item-dealer-lost", numItemsLost, player.m_record);

		auto svReward = GetReward(points);
		if (svReward !is null)
		{
			array<ActorItemQuality> randomQualities;

			auto arrQualities = GetParamArray(UnitPtr(), svReward, "qualities");
			for (uint j = 0; j < arrQualities.length(); j++)
				randomQualities.insertLast(ParseActorItemQuality(arrQualities[j].GetString()));

			int randomIndex = randi(randomQualities.length());
			ActorItemQuality randomQuality = randomQualities[randomIndex];

			auto newItem = g_items.TakeRandomItem(randomQuality);
			if (newItem is null)
			{
				PrintError("Couldn't find a new random item for quality " + randomQuality);
				return;
			}

			if (randomQuality == ActorItemQuality::Legendary)
				Platform::Service.UnlockAchievement("item_dealer_legendary");

			player.AddItem(newItem);

			player.m_record.itemDealerSaved = gm.m_levelCount + 1;
			player.m_record.itemDealerReward = newItem.id;

			ShowFinalItem(newItem);
		}

		ReloadItemList(false);
	}

	void OnFunc(Widget@ sender, string name) override
	{
		if (name == "close")
			Close();
		else if (name == "gamble")
			CommitGamble();
		else if (name == "item-checked")
			UpdateButton();
	}
}

However, we forgot one thing and that's the gui file. I copied mine from "gui/dungeon/itemdealer.gui" from the unpacked base assets and pasted it in my itemtrade mods folder "gui/itemtrade/thyraxxitemtrade.gui" (we are going to modify the layout later.) I put it inside a folder with a different name and also changed the filename to prevent any file collisions.

If we select our mod and load into town (or anywhere else) and press F7, the original itemdealer window should pop up, great! One thing I noticed is that the itemgambler doesn't live update the items only the first time when you open the window, of course we gonna fix this part later.

In the gui we will remove the unnecessary things and add some things to show the players, in this case only a list of playernames you can select. Let's keep it simple!

- Strip down all unnecessary code from gui/class

- Item area bigger

- Change title

- Add player list


After experimenting and changing things aroundI ended up with this gui code:

<gui>
	<sprites>
		<sprite name="checkbutton" texture="gui/widget.png"><frame>346 0 32 32</frame></sprite>
		<sprite name="checkbutton-hover" texture="gui/widget.png"><frame>346 32 32 32</frame></sprite>
		<sprite name="checkbutton-checked" texture="gui/widget.png"><frame>346 64 32 32</frame></sprite>

		<sprite name="buttonfill" texture="gui/widget.png"><frame>378 0 24 24</frame></sprite>
		<sprite name="item-frame" texture="gui/widget.png"><frame>76 182 28 28</frame></sprite>

		<sprite name="quality-frame" texture="gui/widget.png"><frame>410 66 14 14</frame></sprite>
		<sprite name="quality-common" texture="gui/icons.png"><frame>193 57 10 10</frame></sprite>
		<sprite name="quality-uncommon" texture="gui/icons.png"><frame>193 67 10 10</frame></sprite>
		<sprite name="quality-rare" texture="gui/icons.png"><frame>193 77 10 10</frame></sprite>
		<sprite name="quality-epic" texture="gui/icons.png"><frame>193 87 10 10</frame></sprite>
		<sprite name="quality-legendary" texture="gui/icons.png"><frame>193 97 10 10</frame></sprite>

		<sprite name="item-frames" texture="gui/playermenu.png"><frame>335 32 64 32</frame></sprite>
		<sprite name="item-dot" texture="gui/icons.png"><frame>63 0 4 4</frame></sprite>
		<sprite name="item-plus" texture="gui/icons.png"><frame>59 112 5 5</frame></sprite>

		<sprite name="check-item" texture="gui/widget.png"><frame>410 80 16 16</frame></sprite>
		<sprite name="check-item-hover" texture="gui/widget.png"><frame>426 80 16 16</frame></sprite>
		<sprite name="check-item-checked" texture="gui/widget.png"><frame>442 80 16 16</frame></sprite>

		<sprite name="wood" texture="gui/shop.png"><frame>0 53 200 72</frame></sprite>

		<sprite name="icon-dice" texture="gui/icons_others.png"><frame>216 48 24 24</frame></sprite>
		<sprite name="classbutton" texture="gui/widget.png"><frame>33 130 24 24</frame></sprite>

%include "gui/closebutton_sprites.inc"
%include "gui/scalablebutton_sprites.inc"
%include "gui/scalablebutton_big_sprites.inc"
%include "gui/scrollbar_sprites.inc"
%include "gui/scalablebutton_sprites.inc"
	</sprites>

	<doc>
		<group>
			<rect width="320" height="194" anchor="0.5 0.5" spriteset="gui/variable/bigwindow_borders.sval">
				<!-- Content -->
				<rect width="318" height="192" offset="2 2" flow="vbox">
					<!-- Header -->
					<rect width="318" height="18" flow="hbox">
						<!-- Headline -->
						<rect width="294" height="18" spriteset="gui/variable/headline_hor.sval">
							<text font="gui/fonts/arial11_bold.fnt" text="Item trading" anchor="0.5 0.5" />
						</rect>

						<!-- Separator -->
						<rect width="3" height="18" spriteset="gui/variable/3pxbar_vert.sval" />

						<!-- Close button -->
						<spritebutton func="close" spriteset="close" />
					</rect>

					<!-- Separator -->
					<rect width="316" height="3" spriteset="gui/variable/3pxbar_hor.sval" />

					<rect offset="200 0" shadow="#0c120fFF" width="116" height="170" flow="hbox" color="#202a26FF" shadow="#0c120fFF" shadowsize="2">
						<!-- Player list -->
						<scrollrect id="playerlist" flow="hboxwrapped" width="108" height="170" />

						<!-- Scrollbar -->
						<scrollbar forid="playerlist" spriteset="scrollbar" outside="true" buttons-size="14" trough-offset="18" handle-border="2" />
					</rect>

					<!-- Info field -->
					<rect offset="0 -170" width="200" height="18" color="#202a26FF" shadow="#0c120fFF" shadowsize="2">
						<rect width="70" height="18">
							<text offset="2 0" font="gui/fonts/arial11.fnt" text="Select item and a player to give" />
						</rect>
					</rect>

					<!-- Separator -->
					<rect width="200" height="3" spriteset="gui/variable/3pxbar_hor.sval" />

					<!-- Item list -->
					<scrollrect id="list-items" flow="hboxwrapped" width="192" height="128" scroll-backdrop-sprite="item-frames" />

					<!-- Scrollbar -->
					<scrollbar forid="list-items" spriteset="scrollbar" outside="true" buttons-size="14" trough-offset="18" handle-border="2" />

					<!-- Separator -->
					<rect width="200" height="3" spriteset="gui/variable/3pxbar_hor.sval" />

					<!-- Button bar -->
					<rect width="200" height="18" flow="hbox">
						<rect width="65" height="18" spriteset="gui/variable/headline_hor.sval" />

						<!-- Close button -->
						<scalebutton spriteset="scalebutton" width="70" font="gui/fonts/arial11.fnt" text=".itemdealer.close" func="close" />

						<rect width="65" height="18" spriteset="gui/variable/headline_hor.sval" />
					</rect>
				</rect>

				<!-- Item template -->
				<checkbox id="template-item" visible="false" spriteset="check-item" func="item-checked">
					<!-- Icon -->
					<sprite id="item-icon" anchor="0.5 0.5" />

					<!-- Attuned plus -->
					<sprite id="item-attuned" offset="2 2" visible="false" src="item-plus" />

					<!-- Set and quality dots -->
					<group inner="true" anchor="1 1" offset="-2 -2" flow="hbox">
						<sprite id="item-set" visible="false" src="item-dot" />
						<sprite id="item-quality" visible="false" src="item-dot" />
					</group>
				</checkbox>

				<!-- Player template -->
				<scalebutton func="send-items" visible="false" id="player-template" spriteset="scalebutton" width="104" offset="3 5" font="gui/fonts/arial11.fnt" />
			</rect>
		</group>
	</doc>
</gui>


Also, I removed tons of stuff from the ItemTrade class since all those other functions were only for the elements we removed from the GUI.


namespace ItemTradeHook
{
	class ItemTrade : UserWindow
	{
		Widget@ m_wItemList;
		Widget@ m_wItemTemplate;

		ItemTrade(GUIBuilder@ b)
		{
			super(b, "gui/itemtrade/thyraxxitemtrade.gui");

			@m_wItemList = m_widget.GetWidgetById("list-items");
			@m_wItemTemplate = m_widget.GetWidgetById("template-item");

			auto gm = cast<Campaign>(g_gameMode);
			auto record = GetLocalPlayerRecord();

			ReloadItemList();

			UpdateButton();
		}

		void UpdateButton()
		{
			int numSelected = 0;
			for (uint i = 0; i < m_wItemList.m_children.length(); i++)
			{
				auto wItem = cast<CheckBoxWidget>(m_wItemList.m_children[i]);
				if (wItem !is null && wItem.IsChecked())
					numSelected++;
			}
		}

		void ReloadItemList(bool enabled = true)
		{
			m_wItemList.ClearChildren();

			auto record = GetLocalPlayerRecord();
			for (uint i = 0; i < record.items.length(); i++)
			{
				auto item = g_items.GetItem(record.items[i]);
				if (item is null)
				{
					PrintError("Couldn't find item at index " + i);
					continue;
				}

				auto wNewItem = cast<CheckBoxWidget>(AddActorItemToGuiList(record, m_wItemList, m_wItemTemplate, item));
				if (wNewItem !is null)
					wNewItem.m_enabled = enabled;
			}
		}

		void OnFunc(Widget@ sender, string name) override
		{
			if (name == "close")
				Close();
			else if (name == "item-checked")
				UpdateButton();
		}
	}
}	

Let's first fill up the player list, we need a way to somehow get all the players in the server. Like previously, let's search in the docs. Things we can look for are probably something like players or lobby. From Players wasn't anything too interesting, but from Lobby; LobbyPlayersMenu sounds interesting. Let's dive deeper into that class! Hmm.. the function UpdatePlayerList might need all players from the lobby...

int numPlayers = Lobby::GetLobbyPlayerCount();

looks like this gets the amount of players.

for (int i = 0; i < numPlayers; i++)
{
	auto wPlayer = m_wPlayerTemplate.Clone();
	wPlayer.SetID("player-" + i);
	wPlayer.m_visible = true;

	UpdatePlayer(wPlayer, i);

	m_wPlayerList.AddChild(wPlayer);
}

this loops through the amount of players and update the player, if we look at the UpdatePlayer function it accepts a Widget (wPlayer) and a int (peer). The peer can be used for all kinds of stuff, and I *think* we need it and use it as identifier so we know to which person we are going to send our items to.

But first let's find out if we really need the peer, I found GiveItemImpl, but not really what we need. GiveItemImpl function is used by multiple classes, and looking through all of them looks like inside the PlayerHandler class it calls this function. If we look at what function calls this, it's "PlayerGiveItem(uint8 peer, string id)", it wants a peer and item id! Now we know for sure we need the peer.


Let's add the "adding players" part to our class.

I copied the UpdatePlayerList function and pasted it into my own class and modified it slightly for now.

void UpdatePlayerList()
{
	if (!Lobby::IsInLobby())
		return;

	int numPlayers = Lobby::GetLobbyPlayerCount();

	//m_lastNumPlayers = numPlayers;

	m_wPlayerList.ClearChildren();

	for (int i = 0; i < numPlayers; i++)
	{
		auto wPlayer = cast<ScalableSpriteButtonWidget>(m_wPlayerTemplate.Clone());
		wPlayer.SetID("player-" + i);
		wPlayer.SetText("player-" + i);
		wPlayer.m_visible = true;

		//UpdatePlayer(wPlayer, i);


		m_wPlayerList.AddChild(wPlayer);
	}
}


Add the function to the constructor at the bottom and test it in multiplayer, Why multiplayer? Because of this line

if (!Lobby::IsInLobby())
	return;


If we aren't in a lobby players will never be added to the list, you could also temporarily disable it for testing in singleplayer and turning it on later. We should see that a button is added to the list!

Let's fix the problem that the items aren't updated in the window, I have 2 solutions for this, one better than the other but also a bit more work, i'm going the lazy route now. First, we need to somehow call ReloadItems when we open our window, I remember looking inside the UserWindow is has the "Show()" function which our class has been using. This function will run everytime we open our window, so we are going to override the function add our ReloadItems function and then call the base function because we still need to open the window. Still keeping track? xD

void Show() override
{
	UpdatePlayerList();
	ReloadItemList();

	UserWindow::Show(); // Call the base function to open the window
}


For now it shows all players in the lobby, but the button won't do anything yet. First i'm going to fix that we keep track which items are selected and which arent, I slightly modified the UpdateButton function:

at the top of the class I added array<ActorItem> selectedItems;

and change UpdatedButton to:

void UpdateButton()
{
	selectedItems.removeRange(0, selectedItems.length()); // Everytime an item is clicked, we empty the array and fill it again
	for (uint i = 0; i < m_wItemList.m_children.length(); i++)
	{
		auto wItem = cast<CheckBoxWidget>(m_wItemList.m_children[i]);
		auto item = g_items.GetItem(wItem.m_value); // Convert widget to actually a ActorItem type.
		if (wItem !is null && wItem.IsChecked())
		{
			selectedItems.insertLast(item); // if it's check we add the item to the array.
			print(item.id);
		}
	}
}

Now we keep track which items are selected and which aren't! For the last thing, we need to give the items to the player.

I changed the OnFunc and added our own function to send:

void OnFunc(Widget@ sender, string name) override
{
	if (name == "close")
		Close();
	else if(name == "send-items")
	{
		SendItems();
	}
	else if (name == "item-checked")
		UpdateButton();
}


OnFunc is called when the button is clicked and passes "send-items" the text we gave it, this is defined in the gui file in the button. func="send-items"

In our SendItems() function we want to get all the items that we've been keeping track of, loop through it and give it the selected player. If we look inside the ItemDealer again we can find out how items are taken away. player.TakeItem(item);

and put that in our loop Also, as we found out, PlayerHandler::PlayerGiveItem(int peer, string id) to give the player an item, where the peer = online lobby number, and id = item id

If we also put this inside the SendItems function we end up with this:

void SendItems(int peer)
{
	auto player = GetLocalPlayer(); // We get the LocalPlayer (NOT THE PlayerRecord!)
	for(uint i = 0; i < selectedItems.length(); i++)
	{
		player.TakeItem(selectedItems[i]); // Take away the item
		PlayerHandler::PlayerGiveItem(peer, selectedItems[i].id); // Give the item to the selected player
	}
}


Since we also need the peer here, we retrieve that dynamically from the clicked button. Add something to UpdatePlayerList():

for (int i = 0; i < numPlayers; i++)
{
	auto wPlayer = cast<ScalableSpriteButtonWidget>(m_wPlayerTemplate.Clone());
	wPlayer.SetID("player-" + i);
	wPlayer.m_func = "send-items " + i; // Added this, so the "i" (peer) value is appended to the button

	wPlayer.SetText(Lobby::GetPlayerName(i)); // And set the button name we get from the peer, player-0 is kind of awkward.
	wPlayer.m_enabled = (i != lpRecord.peer); // Disable the button if it's client itself, don't wanna send items to yourself.
	wPlayer.m_visible = true; // Could also modify this to show only if it's not the client, whatever is preferred.

	//UpdatePlayer(wPlayer, i);

	m_wPlayerList.AddChild(wPlayer);
}


After a few changes in the OnFunc function:

void OnFunc(Widget@ sender, string name) override
{
	string commandName = name.split(" ")[0];
	if (commandName == "close")
		Close();
	else if(commandName == "send-items")
	{
		int peer = parseInt(name.split(" ")[1]); // Get the peer from the clicked button
		SendItems(peer); // Pass the peer to the function
	}
	else if (commandName == "item-checked")
		UpdateButton();
}


... So I found out after testing, the things in SendItems() won't work correctly, the PlayerGiveItem function isn't what I think it was, I think it's some kind of synchronization for the client, it looks like it doesn't send actually anything... but we can work around this by making our own network function.

I placed in the mods root folder "messages_itemtrade.net" and created this

<!--
    Unreliable
    UnreliableSequenced
    ReliableUnordered
    ReliableSequenced
    ReliableOrdered
-->

<network>

    <message name="GiveItemTrade" namespace="GameModeHandler_itemtrade" delivery="ReliableOrdered">
        <string /> <!-- Item ID -->
    </message>

</network>


Things on the top are just commented, we see a message with the name "GiveItemTrade" which is important to remember, also the namespace="GameModeHandler_itemtrade" which is also important. And of course what we are going to send, in this case an item id which is a string. numbers should be int, etc. Now we create a new GameModeHandler or whatever you want to name it as long it doesn't cause any filename collisions:

// our namespace name
namespace GameModeHandler_itemtrade
{
	// our function name
	void GiveItemTrade(string itemID)
	{
		auto peerPlayer = GetLocalPlayer();
		auto item = g_items.GetItem(itemID);
		peerPlayer.AddItem(item);
		peerPlayer.RefreshModifiers();

		ReloadItemList(); // This function is inside ItemTrade class as a global.
	}
}


the "name" parameter needs to be the same as the function name you are going to give it, it's the same for the "namespace" name for the namespace. With this we can send the function to a peer and tell it to execute this function with certain parameters.

We update the SendItems() function again.

void SendItems(uint peer)
{
	auto player = GetLocalPlayer();

	for(uint i = 0; i < selectedItems.length(); i++)
	{
		auto item = selectedItems[i];
		player.TakeItem(item);

		(Network::Message("GiveItemTrade") << item.id).SendToPeer(peer);
	}

	ReloadItemList();
	UpdateButton();
	player.RefreshModifiers();
}

And we're done!

You can find the complete source here if you want to take a look: https://github.com/thyraxx/item_trader/