Attempting to add right click orders: Trials and Tribulations Part 2: Usurpation

The place to discuss scripting and game modifications for X4: Foundations.

Moderators: Scripting / Modding Moderators, Moderators for English X Forum

morbideth
Posts: 391
Joined: Sun, 9. Nov 08, 03:07
x3tc

Attempting to add right click orders: Trials and Tribulations Part 2: Usurpation

Post by morbideth »

I've been poking around in the lua files to try to figure out if I can add some commands to the right click menu. Its been slow going as I am unfamiliar with lua and there is no documentation about the lua files, not even comments. Below is what I have learned and the roadblocks I've encountered.

Basic info:
The file that controls the right click menus is ui\addons\ego_interactmenu\menu_interactmenu.lua
To mod the file so that it actually works, you have to:
  1. Creating a mod folder in your %installdir%\extensions folder, adding content.xml so it can load.
  2. Create the directory structure "ui\addons\ego_interactmenu\" in your mod folder
  3. Copy the menu_interactmenu.lua into the ego_interactmenu of your mod
  4. Edit menu_interactmenu.lua with the desired changes
  5. Make a copy of the edited menu_interactmenu.lua and rename it menu_interactmenu.xpl
  6. Create a subst_01.cat file from the ui folder. Removing the loose files from the mod folder itself, so that you are left with just subst_01.cat, subst_01.dat, and content.xml in your mod folder (plus what ever other files your mod has, like aiscripts)
Because it is in a cat file, you must completely exit to desktop and relaunch the game every time you make changes. (creating a new cat, ect.) As you might imagine this makes doing anything with the files rather tedious. But I was able to confirm this works, by editing the text of some of the commands on the menu.

The code:

The meat of the code I want to change seems to be in three main areas:
First is a big long ifelse list (what would be a switch statement in other languages)

function menu.insertLuaAction(actiontype, istobedisplayed)

Each statement is something like:

Code: Select all

elseif actiontype == "collect" then
		if #menu.selectedplayerships > 0 and menu.possibleorders["Collect"] and menu.componentSlot.component and C.IsComponentClass(menu.componentSlot.component, "drop") then
			menu.insertInteractionContent("selected_orders", { type = actiontype, text = ReadText(1001, 7867), script = function () return menu.buttonCollect(false) end, hidetarget = true } )
		end
		
We have the actiontype == "collect", that is passed to the function.
Then we have the conditions for if the order is listed. In this case, if a player ship is slected before we right click, if menu.possibleorders["Collect"], if (what we clicked on exist, i.e. is not nil), and if what we right-clicked on isClass "drop".
After that we add the entry to the menu with menu.insertInteractionContent

"selected_orders"
is basically a category.

type = actiontype
is passed from caller

text = ReadText(1001, 7867)
is the label of the order

script = function () return menu.buttonCollect(false) end
is what happens when we click on the order.

So two things to look at from here, first is "menu.possibleorders["Collect"]"
It is initialized with:

Code: Select all

menu.possibleorders = {
		["Attack"] = false,
		["Board"] = false,
		["Collect"] = false,
		["CollectDropsInRadius"] = false,
		["DeployObjectAtPosition"] = false,
		["DockAndWait"] = false,
		["Explore"] = false,
		["ExploreUpdate"] = false,
		["Flee"] = false,
		["Follow"] = false,
		["MiningPlayer"] = false,
		["MoveWait"] = false,
		["Player_DockToTrade"] = false,
		["ProtectStation"] = false,
		["Repair"] = false,
		["TradeRoutine"] = false,
	}
	
The string portion comes straight from the aiscript file, in this case order.collect.ship.xml

Code: Select all

<order id="Collect" name="{1041, 481}" description="{1041, 482}" category="trade">
Then the true/false values are set:

Code: Select all

			for orderid, value in pairs(menu.possibleorders) do
				if not value then
					if C.IsOrderSelectableFor(orderid, ship) then
						menu.possibleorders[orderid] = true
					end
				end
			end
IsOrderSelectableFor is undocumented, but seems to check the aiscript file to see if the order is valid for the selected ship. I was able to create a, essentially actionless, aiscript order that evaluated to true by IsOrderSelectableFor.

Now onto menu.buttonCollect(false)

Code: Select all

function menu.buttonCollect()
	for _, ship in ipairs(menu.selectedplayerships) do
		menu.orderCollect(ship, menu.componentSlot.component, menu.offsetcomponent, menu.offset, clear)
	end

	menu.onCloseElement("close")
end
Which just calls menu.orderCollect for every ship. menu.orderCollect is:

Code: Select all

function menu.orderCollect(component, drop, sector, offset, clear)
	if not C.IsOrderSelectableFor("Collect", component) then
		return
	end

	if clear then
		C.RemoveAllOrders(component)
	end
	local orderidx = C.CreateOrder(component, "Collect", false)
	SetOrderParam(component, orderidx, 1, 0, ConvertStringToLuaID(tostring(drop)) )
	C.EnableOrder(component, orderidx)

	return orderidx
end
First we have a double check to see if the order is selectable, then an option to clear/cancel the order, and finally where it actually orders the ship.
I believe C.CreateOrder(component, "Collect", false) is the same as <create_order object="" id="" immediate=""/>
SetOrderParam is simular to <edit_order_param object="" order="" param="(as a index of all params)" (looks like the index of value when the param takes a list) value=""/>

Theoretically we should now be able to add our own order to the right click menu. However, here is where I run into trouble. The first thing we looked at, menu.insertLuaAction(actiontype, istobedisplayed), is called here:

Code: Select all

local n = C.GetNumCompSlotPlayerActions(menu.componentSlot)
		local buf = ffi.new("UIAction[?]", n)
		n = C.GetCompSlotPlayerActions(buf, n, menu.componentSlot)
		for i = 0, n - 1 do
			local entry = {}
			entry.id = buf[i].id
			entry.text = ffi.string(buf[i].text)
			entry.active = buf[i].ispossible
			local actiontype = ffi.string(buf[i].type)
			if (not menu.shown) and (actiontype == "containertrade") then
				entry.script = function () return menu.buttonTrade(false) end
			elseif (not menu.shown) and (actiontype == "info") then
				entry.script = menu.buttonInfo
			elseif (not menu.shown) and (actiontype == "comm") then
				entry.script = menu.buttonComm
			else
				entry.script = function () return menu.buttonPerformPlayerAction(entry.id, actiontype) end
			end

			local basetype, luatype = string.match(actiontype, "(.+);(.+)")
			if isknown or (actiontype == "info") or (luatype == "guidance") then
				if basetype == "lua" then
					menu.insertLuaAction(luatype, buf[i].istobedisplayed)
C.GetCompSlotPlayerActions returns a struct containing type from which luatype is parsed. The format is 'lua;upgrade' with id=nil or something like 'containertrade' and id=Trade. The problem is I have no idea where GetCompSlotPlayerActions gets its values. They don't map to anything we have access to. Nor do I see anyway to add to the existing list so that more items can be added to the menu. The only way I can, at this point, change the menu is by high-jacking one of the other luatypes. i.e. edit the script in

Code: Select all

menu.insertInteractionContent("selected_orders", { type = actiontype, text = ReadText(1001, 7867), script = function () return menu.buttonCollect(false) end, hidetarget = true } ) 
to call menu.buttonMyMod(false)

I hope this information is helpful for those looking into modding the UI, and if you have any information to add, please do so.

Part 2: Usurpation

Well if the only way we can add right click orders is to pirates some actiontypes then we might as well break out the jolly roger and get to it.

I added some debug output and went through clicking on a bunch of stuff to see what actiontypes get called when. It turns out that "guidance" gets passed in almost every case. The only exception being when you click on gates. Thus I chose this to area to modify adding my own order "CollectDeployables" to the entry, while leaving the original code alone.

Code: Select all

	elseif actiontype == "guidance" then
		--Modded entry
		if menu.possibleorders["CollectDeployables"] 
				and #menu.selectedplayerships > 0 
				and menu.componentSlot.component 
				and isplayerownedtarget and (
												(
													C.IsComponentClass(menu.componentSlot.component, "satellite") 
													or C.IsComponentClass(menu.componentSlot.component, "resourceprobe") 
													or C.IsComponentClass(menu.componentSlot.component, "navbeacon")
												) 
												or GetComponentData(ConvertStringTo64Bit(tostring(menu.componentSlot.component)), "shiptype") == "lasertower") then
			menu.insertInteractionContent("selected_orders", { type = actiontype, text = ReadText(1042, 400), script = function () return menu.buttonCollectDeployables(false) end })
		end
		--end modded changes
		if menu.mode ~= "shipconsole" then
			if C.IsComponentClass(menu.componentSlot.component, "sector") then
				menu.insertInteractionContent(menu.showPlayerInteractions and "player_interaction" or "interaction", { type = actiontype, text = IsSameComponent(GetActiveGuidanceMissionComponent(), convertedComponent) and ReadText(1001, 3243) or ReadText(1001, 3242), script = function () return menu.buttonGuidance(true) end })
			else
				menu.insertInteractionContent(menu.showPlayerInteractions and "player_interaction" or "interaction", { type = actiontype, text = IsSameComponent(GetActiveGuidanceMissionComponent(), convertedComponent) and ReadText(1001, 3243) or ReadText(1001, 3256), script = function () return menu.buttonGuidance(false) end })
			end
		end
Next I added the button and order functions:

Code: Select all

function menu.buttonCollectDeployables(clear)
	for _, ship in ipairs(menu.selectedplayerships) do
		menu.orderCollectDeployables(ship, menu.componentSlot.component, clear, ventureplatform)
	end

	menu.onCloseElement("close")
end

function menu.orderCollectDeployables(component, target, clear, ventureplatform)
	if not C.IsOrderSelectableFor("CollectDeployables", component) then
		return
	end

	if clear then
		C.RemoveAllOrders(component)
	end
	local orderidx = C.CreateOrder(component, "CollectDeployables", false)
	--Param 1: target deployable
	SetOrderParam(component, orderidx, 1, nil, ConvertStringToLuaID(tostring(target)))
	C.EnableOrder(component, orderidx)

	return orderidx
end
Finally I added the entry ["CollectDeployables"]= false to menu.possibleorders

This leaves me with a fully functioning right click command:
[ external image ]

However, this method is not a good solution since only one mod can make these changes. Ideally we want to create some hooks into the vanilla code to inject the above changes into the core game from some modded files. I don't know how to do this at this time. I do feel it should be possible though. We have the ui.xml file that can be edited with a diff patch to add more files to its list. Theses files are loaded when a save is loaded so you can, at the very least, move the functions to other files (I think, I've not tested this theory). There is also the OpenMenu() function that should allow you to call things from those new files. Dofile() and require() are also options.

What we need, however, is a way to hook into the vanilla files to edit menu.possibleorders (menu being a local variable) and inject code into menu.insertLuaAction(actiontype, istobedisplayed). Something like a global table all files in the ui.xml file could access, with things like Table("OrderID", Condition, Extra_Code, menu.insertInteractionContent arguments)
Then have something like:

Code: Select all

elseif actiontype == "guidance" then
	for i=0 to Table.count
		if menu.possibleorders[Table[i].OrderID] and Table[i].Condition then
			Table[i].Extra_Code
			menu.insertInteractionContent(Table[i].arguments)
		end
	end
	...vanilla code
This is however, beyond my current knowledge level. If anyone knows how we might go about doing this, feel free to enlighten us.
Last edited by morbideth on Sat, 22. Dec 18, 22:50, edited 1 time in total.
morbideth
Posts: 391
Joined: Sun, 9. Nov 08, 03:07
x3tc

Re: Attempting to add right click orders: Trials and Tribulations

Post by morbideth »

OK, finished.
daego
Posts: 36
Joined: Tue, 2. Sep 08, 20:07
x4

Re: Attempting to add right click orders: Trials and Tribulations

Post by daego »

sry, just one info.. cant wait for this mod ! :!:
TheDeliveryMan
Posts: 867
Joined: Sat, 10. Dec 11, 03:10
x4

Re: Attempting to add right click orders: Trials and Tribulations

Post by TheDeliveryMan »

Well, I'm really glad that default order scripts are registered automatically, so I don't have to deal with lua for my mod.
Antaran
Posts: 166
Joined: Wed, 6. Nov 02, 20:31
x3tc

Re: Attempting to add right click orders: Trials and Tribulations

Post by Antaran »

morbideth wrote: Thu, 20. Dec 18, 19:44 .....

Because it is in a cat file, you must completely exit to desktop and relaunch the game every time you make changes. (creating a new cat, ect.) As you might imagine this makes doing anything with the files rather tedious. But I was able to confirm this works, by editing the text of some of the commands on the menu.

.......

Have you looked at some of the mentions here? [*][Index] X4: Foundations Tools, Tutorials and Resources

1 post mentions how to reload the ui while ingame, another that has code complete guide for visual studio that might be applied to the ui developmentwork?
morbideth
Posts: 391
Joined: Sun, 9. Nov 08, 03:07
x3tc

Re: Attempting to add right click orders: Trials and Tribulations

Post by morbideth »

Antaran wrote: Fri, 21. Dec 18, 22:57 Have you looked at some of the mentions here? [*][Index] X4: Foundations Tools, Tutorials and Resources

1 post mentions how to reload the ui while ingame, another that has code complete guide for visual studio that might be applied to the ui developmentwork?
The reload command does not work with cat files. That is why I explicitly mentioned it.

The visual studio guide has nothing to do with ui modding.
DeadMor0z
Posts: 26
Joined: Fri, 29. Nov 13, 18:20
x4

Re: Attempting to add right click orders: Trials and Tribulations Part 2: Usurpation

Post by DeadMor0z »

Have you looked into XR UI modding sipport? Since X4 is based on XR engine, most of information fo UI modding in XR applies to X4.
There are examples how to write your hooks, list of LUA and C functions and code examples.
morbideth
Posts: 391
Joined: Sun, 9. Nov 08, 03:07
x3tc

Re: Attempting to add right click orders: Trials and Tribulations Part 2: Usurpation

Post by morbideth »

I have. It's mostly a list of functions without a description of what they do, reminds me a lot of the in game encyclopedia in X4. There is one partial example of how to replace an existing menu with a new one, which isn't what we want to do. The forums threads have some useful info, might be more, I haven't read every page, but I doubt it has what we want.
DeadMor0z
Posts: 26
Joined: Fri, 29. Nov 13, 18:20
x4

Re: Attempting to add right click orders: Trials and Tribulations Part 2: Usurpation

Post by DeadMor0z »

morbideth wrote: Sun, 23. Dec 18, 00:39 I have. It's mostly a list of functions without a description of what they do, reminds me a lot of the in game encyclopedia in X4. There is one partial example of how to replace an existing menu with a new one, which isn't what we want to do. The forums threads have some useful info, might be more, I haven't read every page, but I doubt it has what we want.
Are you sure you are talking about that link? Most functions have a short description, describing what they do, many have parameters description also.
About hooking - have you looked at this thread? On the second page egosoft developer shows two examples of hooking.
morbideth
Posts: 391
Joined: Sun, 9. Nov 08, 03:07
x3tc

Re: Attempting to add right click orders: Trials and Tribulations Part 2: Usurpation

Post by morbideth »

DeadMor0z wrote: Sun, 23. Dec 18, 01:20 Are you sure you are talking about that link? Most functions have a short description, describing what they do, many have parameters description also.
About hooking - have you looked at this thread? On the second page egosoft developer shows two examples of hooking.
Turns out my browser was not displaying the descriptions, I have the page zoomed in a bit and it was clipping off the descriptions.

As for the hooks, I had not seen that. It's useful. Not sure it is quite what we want. It seems we should be able to use the first version of the hook to access menu.possibleorders. menu.insertLuaAction, on the other hand, is something we only want to change part of. It may be necessary to rewrite the function to do what we wish. I'd rather not as that would make maintaining the code more difficult. Maybe we can make an event type thing for a hook. Like onActionTypeGuidance. I don't know enough about lua scope to be sure what is possible in that regard. We want access to variables that are both local to the function and ones that are local to the file (I think).
DeadMor0z
Posts: 26
Joined: Fri, 29. Nov 13, 18:20
x4

Re: Attempting to add right click orders: Trials and Tribulations Part 2: Usurpation

Post by DeadMor0z »

morbideth wrote: Sun, 23. Dec 18, 03:06
DeadMor0z wrote: Sun, 23. Dec 18, 01:20 Are you sure you are talking about that link? Most functions have a short description, describing what they do, many have parameters description also.
About hooking - have you looked at this thread? On the second page egosoft developer shows two examples of hooking.
Turns out my browser was not displaying the descriptions, I have the page zoomed in a bit and it was clipping off the descriptions.

As for the hooks, I had not seen that. It's useful. Not sure it is quite what we want. It seems we should be able to use the first version of the hook to access menu.possibleorders. menu.insertLuaAction, on the other hand, is something we only want to change part of. It may be necessary to rewrite the function to do what we wish. I'd rather not as that would make maintaining the code more difficult. Maybe we can make an event type thing for a hook. Like onActionTypeGuidance. I don't know enough about lua scope to be sure what is possible in that regard. We want access to variables that are both local to the function and ones that are local to the file (I think).
Hook for menu.insertLuaAction is quite simple, something like that:

Code: Select all

local orig = {}
local function myInsertLuaAction(actiontype, istobedisplayed)
	-- call original function first
	orig.insertLuaAction(actiontype, istobedisplayed)
	-- then process your own action
	if action == ... then
	end
end

local function init()
   for _, menu in ipairs(Menus) do
       if menu.name == "InteractMenu" then
             orig.menu = menu -- save entire menu, for other helper function access
       	     orig.insertLuaAction = menu.insertLuaAction -- save original function
       	     menu.insertLuaAction = myInsertLuaAction -- replace called function with you own
          break
      end
   end
end

init()
as of menu.possibleorders - it is not persistent and is recreated on each function call. The only way - it to replace that function entirely with you own.
morbideth
Posts: 391
Joined: Sun, 9. Nov 08, 03:07
x3tc

Re: Attempting to add right click orders: Trials and Tribulations Part 2: Usurpation

Post by morbideth »

It seems we cannot patch the ui.xml file. Thus, the only way to apply the above changes is to replace the file with a subst cat, which leaves back at square one. More thought is required.

Return to “X4: Foundations - Scripts and Modding”