Friday, September 3, 2021

Why My Mods Didn't Support Controllers

Twitch streamer Djackzz discovered that my mods (in particular Playable Arcade Machines, Simple Playable Pianos, and Silly Object Sounds) didn't work with the controller.  He discovered this while playing co-op on his stream.

Since not many people play Project Zomboid co-op, I didn't consider this a high priority.  Plus, before 41.51 I couldn't get my Xbox One controller to work with Project Zomboid (I use Linux as my OS).

Since 41.53 has been out for over a month now I decided it was time to find my Xbox One controller and fix this bug.  It took me over an hour to find my controller, because instead of being in the first places I looked (with my cables, with my extra computer parts, in a bin with assorted tech junk), it was on top of my Xbox.

Playable Arcade Machines was my first mod, and one of the most important things my mod does is create a menu item to "Play" the particular game.  I don't remember where I looked for examples of that, but the good examples basically looked like this one that I'm taking from the ISBBQMenu:

function ISBBQMenu.OnFillWorldObjectContextMenu(player, context, worldobjects, test)

	if test and ISWorldObjectContextMenu.Test then return true end

	local bbq = nil

	local objects = {}
	for _,object in ipairs(worldobjects) do
		local square = object:getSquare()
		if square then
			for i=1,square:getObjects():size() do
				local object2 = square:getObjects():get(i-1)
				if instanceof(object2, "IsoBarbecue") then
					bbq = object2
				end
			end
		end
	end

	if not bbq then return end

Not knowing anything about Project Zomboid mods I guessed the following:

  • player - Was an object related to the player
  • context - had to do something with the "context menu" I was trying to add my commands to
  • worldobjects - some kind of key value pair object containing a bunch of objects (probably near the character)
  • test - test variable I didn't need to worry about

Briefly, this code cycles through all of the worldobjects to see if any of them are on squares that also contain a BBQ.  Once the code figures out which object is the BBQ, it can then add the menu item for the BBQ Info.

But I also saw some mod examples that didn't go through this whole process to find the object it wanted to activate.  Instead of looping through a bunch of worldobjects, they just picked the first one:

	local thisObject = worldobjects[1];

When I tested out using just this in my Playable Arcade Machines mod, it worked.  And from a time-complexity standpoint, it was much better.  I didn't know if worldobjects was large or small.  If it were large, it seemed like a huge waste of time to search through all of the values in worldobjects when worldobjects[1] had what I wanted!  I mean, just the name "worldobjects" implies it could be everything in the world!  So I did the same for my piano mod, and for my silly object sounds mod.

But for controller it didn't work, I would later find.  A mouse can usually pick the exact object I want to activate, so worldobjects[1] usually works.  

But the controller can't know the exact pixel you've selected - you can only stand near the object you want to activate.  You might be standing in the right spot, and pointing to the right object.  But it's really hard to tell.  Through experimentation it looks to me like worldobjects[1] is the ground square in front of the character all the time, and I don't really think worldobjects[1] will ever be the arcade machine (or piano) in front of the character.  Also in my experiments, it looks like it only pulls around 5 objects into worldobjects, so it isn't examining too many more squares.

So for my Simple Playable Pianos mod, I changed it to loop through all the worldobjects:

	for _,object in ipairs(worldobjects) do
		local square = object:getSquare()
		if square then
			for i=1,square:getObjects():size() do
				local thisObject = square:getObjects():get(i-1)
				
				local properties = thisObject:getSprite():getProperties()

				if properties ~= nil then
					local groupName = nil
					local customName = nil
					local thisSpriteName = nil
					
					local thisSprite = thisObject:getSprite()
					if thisSprite:getName() then
						thisSpriteName = thisSprite:getName()
						--print("PseudoEdSPP: Sprite Name is " .. spriteName)
					end
					
					if properties:Is("GroupName") then
						groupName = properties:Val("GroupName")
						--print("PseudoEdSPP: Sprite GroupName: " .. groupName);
					end
					
					if properties:Is("CustomName") then
						customName = properties:Val("CustomName")
						--print("PseudoEdSPP: Sprite CustomName: " .. customName);
					end
					
					-- For Pianos, Custom Name = "Piano".
					-- In vanilla PZ, GroupName = "Western", but leave that open for modders
					if customName == "Piano" then
						piano = thisObject;
						spriteName = thisSpriteName;
					end
				end
			end
		end
	end

So now it follows these basic steps:

  • for each of the world objects
  • select the square the object is on
  • check all the objects on that square, looking for a piano

And if if finds the object is a piano, it stores it as piano so I can add the menu items later...

It might be cycling through a few more objects than necessary for mouse users, but this is necessary for controllers.  I'll be fixing the rest of my mods in the next few days.


Thanks for reading!  If you notice any mistakes, or if you have any questions you'd like me to answer, please let me know!


I'm trying to do some modding so I may not be posting as often as I have been recently.  But maybe I will post.  I'm trying to figure it out.



No comments:

Post a Comment

Do Gloves Protect You From Broken Glass?

Yes, gloves protect you from handling broken glass - any pair of gloves.  But gloves are not needed when removing broken glass from a smashe...