[API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.32 (02/27/19))

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

[API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.32 (02/27/19))

Post by morbideth » Wed, 2. Jan 19, 07:33

If you are not a modder, you can skip to the download below, just READ THE INSTALL INSTRUCTIONS. Ah, who am I kidding, no one is going to rtfm.

MUST BE INSTALLED IN YOUR %installdir%\extensions folder! REQUIRES: basic literacy to install.
v0.32 Right Click API, _G Work Around, Modding Templet, and Debug Tools
[ external image ]
  • You may NOT: Host these files elsewhere without either my permission or if I have been inactive on these forums for over a month.
  • You may NOT: Include the Right Click API or the _G Work Around with your mod until version 1.0 or later of this API
  • You may NOT: Under any circumstances move the Right Click API or the _G Work Around mods to another folder when adding with your mod. You must keep them as their own separate mod so people can see which version they have installed.
  • You may NOT: Under any circumstances include the debugging tools in a release of your mod.
For modders, How to mod the UI. First off, for those who don't know, what's the issue with modding the UI currently? The issue is that _G is null. _G is where LUA stores all of its global variables. This means that if we mod the way we were meant to, we do not have access to any of the menus to mod. The only way, until now, was to change core game files. Meaning only one mod could make the changes and that they would be difficult to maintain. However, I've come up with a simple, and easy to use work around that will allow you to load your file inside the 'core' environment so that you have access to all the global variables. To do so you will need to download the API at the bottom and then create an MD script. As an example, I will use my Show Ranks mod, as it is simple.

First the MD script (you can find templet files in the download below)
Spoiler
Show

Code: Select all

    <cue name="RegisterShowRanks" version="4" instantiate="true">
      <conditions>
        <event_game_loaded />
      </conditions>
	  <delay exact="3s"/>
      <actions>
        <signal_cue_instantly cue="md.G_Work_Around.Signal" param="'.\\extensions\\ShowRanks\\ShowRanks.lua'"/>
        <reset_cue cue="Feedback_Wanted"/>
      </actions>
    </cue>
    <cue name="Feedback_Wanted">
      <conditions>
        <event_cue_completed cue="RegisterShowRanks"/>
      </conditions>
      <delay exact="1s"/>
      <actions>
        <debug_text text="'Show Ranks Cue Loaded'" filter="error"/>
        <reset_cue cue="RegisterShowRanks"/>
        <reset_cue cue="Feedback_Wanted"/>
      </actions>
    </cue>
Here we have the cue RegisterShowRanks that triggers when the game loads. What it does is <signal_cue_instantly cue="md.G_Work_Around.Signal" param="'.\\extensions\\ShowRanks\\ShowRanks.lua'"/> Sending a signal with the file name to the MD files in the work around mod which opens a menu called 'G_Work_Around' at 0, 0 with the parameter of a file path. Note the double slashes for an escape sequence. Also note that LUA is case sensitive, and this means that your mod MUST be installed in the %installdir%\extensions folder. Finally, you should note that the Lua file 'ShowRanks.lua' is in the root directory of the mod. The game ignores Lua files in this location, so by placing it here, we ensure that it will not conflict with anything when the _G issue is finally fixed.
You may notice that there is also a short delay before the action. While not necessary for this particular mod, it is a good idea to include a delay if you have dependencies. Such as if you are using the right click menu API included in this download.
The cue Feedback_Wanted is reset by RegisterShowRanks and triggered when RegisterShowRanks is complete. Then Feedback_Wanted prints to the debug log and resets RegisterShowRanks. You could have RegisterShowRanks reset itself, but I left this debug text in to show you how you might ensure that your Lua file is being loaded.

Next we have the lua file itself.
Spoiler
Show

Code: Select all

local orig = {}
local addRanks = {}

local function init()
	--DebugError("Rank Display mod init")
   for _, menu in ipairs(Menus) do
       if menu.name == "MapMenu" then
             orig.menu = menu -- save entire menu, for other helper function access
			 orig.playerInfoTextLeft=Helper.playerInfoTextLeft
			 orig.playerInfoTextRight=Helper.playerInfoTextRight
			 Helper.playerInfoTextLeft=addRanks.playerInfoTextLeft 
			 Helper.playerInfoTextRight=addRanks.playerInfoTextRight
          break
      end
   end
end

function addRanks.playerInfoTextLeft()
	local playername = ffi.string(C.GetPlayerName())
	local playermoney = ConvertMoneyString(GetPlayerMoney(), false, true, nil, true) .. " " .. ReadText(1001, 101)
	local gametime = Helper.convertGameTimeToXTimeString(C.GetCurrentGameTime()) .. (C.IsSetaActive() and " \27G(" .. ReadText(1001, 3255) .. ")\27X" or "")
	local stats = GetAllStatIDs()
	return playername .. "\n" ..  GetStatData(stats[98], "displayvalue") .. "\n" ..  GetStatData(stats[101], "displayvalue") .. "\n" .. playermoney
end

function addRanks.playerInfoTextRight()
	local curtime = getElapsedTime()
	local playersector = C.GetContextByClass(C.GetPlayerID(), "sector", false)
	-- expensive, only check every 5 seconds
	if (not Helper.playerInfoTimer) or (Helper.playerInfoTimer < curtime) then
		Helper.playerInfoTimer = curtime + 5
		Helper.rawplayermoneydue = tonumber(C.GetCreditsDueFromPlayerTrades())
	end
	local playermoneydue = ConvertMoneyString(Helper.rawplayermoneydue, false, true, nil, true) .. " " .. ReadText(1001, 101)
	local gametime = Helper.convertGameTimeToXTimeString(C.GetCurrentGameTime()) .. (C.IsSetaActive() and " \27G(" .. ReadText(1001, 3255) .. ")\27X" or "")
	return "\n" .. ffi.string(C.GetComponentName(playersector)) .. "\n" .. gametime .. "\n" ..((Helper.rawplayermoneydue > 0) and ("(" .. string.format(ReadText(1001, 7704), playermoneydue) .. ")") or "")
end

init()
First, we need to find which menu holds the elements we want to change. For Show Rank, this was MapMenu (actually global, but just pretend). Thus in the init we search for the MapMenu

Code: Select all

for _, menu in ipairs(Menus) do
       if menu.name == "MapMenu" then
Once we find it, we use the 'correct' way to mod file. First we save the vanilla functions to the table orig declared at the top of the file.

Code: Select all

orig.playerInfoTextLeft=Helper.playerInfoTextLeft
orig.playerInfoTextRight=Helper.playerInfoTextRight
Then we overwrite them with our modded functions defined at the bottom of the file:

Code: Select all

Helper.playerInfoTextLeft=addRanks.playerInfoTextLeft 
Helper.playerInfoTextRight=addRanks.playerInfoTextRight
NOTE: This would normally be, and should in most cases be menu.playerInfoTextLeft/Right in place of Helper.playerInfoTextLeft/Right. It is Helper in this case because it is a global function, Helper being global.

Now we write our functions we replaced. As I said they are at the bottom. Ideally, we would include a call to the original function, i.e. orig.playerInfoTextLeft(), inside of our new function. However, in this case, that isn't possible. But you should endeavor to include such a call whenever feasible. This ensures that other mods can change other elements of that particular menu or function without causing compatibility issues. Modding it with this method ensures that it will continue to be compatible with future updates. When the _G issue is fixed, all you will have to do is make a ui.xml file and move the Lua file to a subfolder in your mod. Now you can do something about the horrible UI... please?

As adding commands/actions to the right-click menu ("InteractMenu") is more involved, I've included an API for adding your own right-click commands while playing well with others. This API uses the above work around to load, so it needs to be installed in the %installdir%\extensions folder. I'll be using my Collect Deployables mod as a brief example of how to use the API. If you want a more in-depth discussion you can look here: viewtopic.php?f=181&t=409723

Code in 2nd post

To add a new command (or menu or whatever) to the right-click menu we need 3 things. 1 a check, usually involving an order. This takes the form of

Code: Select all

if capi.possibleorders["CollectDeployables"] 
the capi. prefix refers to the API and makes this variable global so you can access it. The string "CollectDeployables" is from the aiscript order id:

Code: Select all

<order id="CollectDeployables" name="{1042, 400}" description="{1041, 401}" 
Note that while its name is possibleorder and it should check to see if the order is valid, this does not function correctly. This is why you were able to have Auto-Traders from the right-click menu without them having 3 stars. You can see how I added my order to the possibleorder table in the init function with

Code: Select all

capi.possibleorders["CollectDeployables"] = false
Immediately following that I also register the function CollectDeployablesAction via

Code: Select all

capi.RegisterGuidanceAction(CollectDeployablesAction)
This function will encapsulate all three components of our order: the check, the button, and the action.

In this case the check is a bit long:

Code: Select all

if capi.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
Simply put: Is the order possible and do we have at least one player ship select and is this not null and did we click on a player owned object and is it a (sat, resource probe, nav beacon, or lasertower)? If so we add our button to the menu with the next line:

Code: Select all

menu.insertInteractionContent("selected_orders", { type = actiontype, text = ReadText(1042, 400), script = function () return menu.buttonCollectDeployables(false) end })
NOTE: since we registered the function CollectDeployablesAction it is passed the 'menu' variable from the InteractMenu which is how we are able to use variables like menu.selectedplayerships

Finally, the action is performed by menu.orderCollectDeployables. We create the order with:

Code: Select all

local orderidx = C.CreateOrder(component, "CollectDeployables", false)
And set the paramaters with

Code: Select all

 --Param 1: the deployable, macro is derived from this.
	SetOrderParam(component, orderidx, 1, nil, ConvertStringToLuaID(tostring(target))
The 1 is in the nth parameter as defined in the aiscript file. The nil is the index of the list if the parameter is a list.
VERY IMPORTANT: THIS DOES NOT CHECK input_params!!!
Look at order.collect.deployables, the first parameter has the input params of:

Code: Select all

<param name="targetDeployable" type="object" text="{1041, 10027}" comment="The deployable to collect, macro and sector is taken from this object">
        <input_param name="class" value="[class.navbeacon, class.resourceprobe, class.satellite]"/>
        <input_param name="owner" value="faction.player"/>
      </param>

the class must be either navbeacon, resourceprobe, satellite. But, if you recall from above, we are also passing lastertowers to the script. Lasertowers are class ship. But the lua file ignores this and passes them anyway. Therefore it is VERY important to make sure the check section of your Lua file is rigorous to prevent passing invalid data to your script and causing horrible errors.

You should add a dependency to your mod, in the content.xml, when you use the APIs shown below.

Code: Select all

<content id=......>
 <dependency id="G_Work_Around" version="031" optional="false"/>
 <text language=..../>
</content>
Information on subsections and debugging can be found in the second post.
Last edited by morbideth on Wed, 27. Feb 19, 14:21, edited 13 times in total.

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

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.31

Post by morbideth » Wed, 2. Jan 19, 07:33

Changelog
  • v0.32
    • Updated the right click api to work with 2.0.
    • Added 'showTypes' features to debugging tools, intended for use with the right-click menu. add capi.showTypes = true to your init() function to print the action types when you right-click on something. Use this to determine which action type you want to use to trigger your right-click menu.
    • Added right-click stuff to templet
  • v0.31
    • Incorporated changes by DeadMor0z to use LUA events. This removes some debug spam and solves some issues with reloading the UI. \reloadui will now work with mods using the API. All previous versions using the menu or signals are depreciated and will no longer function. Ensure that your mods are updated before using this version.
    • Right click API added support for custom sections and subsections, see below for more details.
    • Debug Tools: Enabled the Cheat menu. This accessible from the left-hand bar, on the map menu.
    • Moved templet to the debug tools zip, renamed zip to modding tools.
    • Add content.xml with dependancy entries to the templet.
  • v0.20
    • Added a MD file to solve issues with multiple mods trying to load their mod at the same time, by UniTrader. See the templet or the post above for new method of using the _G work around.
    • Changed the templet to follow the above standard.
    • Added UI debugging tools to work with the _G Work Around, see below.
  • v0.11 Fixed Right click API breaking some vanilla commands
  • v0.10 Initial release
Known Issues
  • none
CollectDeployables.lua
Show

Code: Select all

colDep = {}
menu = nil

local function init()
	DebugError("CollectDeployables init")
	for _, menu in ipairs(Menus) do
       if menu.name == "InteractMenu" then
			colDep = menu -- save entire menu, for other helper function access
			break
	   end
    end
	   
	capi.possibleorders["CollectDeployables"] = false
	capi.RegisterGuidanceAction(CollectDeployablesAction)
end

function CollectDeployablesAction(menu)
local convertedComponent = ConvertStringTo64Bit(tostring(menu.componentSlot.component))
local isplayerownedtarget = GetComponentData(convertedComponent, "isplayerowned")

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: the deployable, macro is derived from this.
	SetOrderParam(component, orderidx, 1, nil, ConvertStringToLuaID(tostring(target)))
	C.EnableOrder(component, orderidx)
	return orderidx
end

function menu.buttonCollectDeployables(clear, ventureplatform)
	local convertedComponent = ConvertStringTo64Bit(tostring(menu.componentSlot.component))
	local convertedVenturePlatform = ventureplatform and ConvertStringTo64Bit(tostring(ventureplatform))
	for _, ship in ipairs(menu.selectedplayerships) do
		menu.orderCollectDeployables(ship, menu.componentSlot.component, clear, ventureplatform)
	end

	menu.onCloseElement("close")
end
--DebugError("CollectDeployables Action #menu.selectedplayerships" .. #menu.selectedplayerships .. " " .. tostring(menu.componentSlot.component) .. " " .. tostring(isplayerownedtarget) .. " " .. tostring(C.IsComponentClass(menu.componentSlot.component, "satellite")) )
if capi.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		

init()
Tips and Quirks:
A section for oddities and stumbling blocks that you might come across when writing lua scripts.
  • "knownname" doesn't work for GetComponentData, "name" does.
  • Having the chat window open can break certain elements of the UI
  • OpenMenu and its derivatives tries to convert the params to a script value which makes passing functions impossible.
  • True and false are 0 and 1 in scripts, in LUA 0 and 1 are just numbers, both of which evaluate to true.
Right click API: custom sections and subsection:
What are sections and subsection? A section would be like:
[ external image ]


And a subsection:
[ external image ]

To add a section is easy, all you to do is add the following to your init function:
wrote:table.insert(capi.config.sections, { id = "ComAgent", text = ReadText(8000, 1000), isorder = true })
isorder is well, if it is an order or not. If the section will show up in the same menu orders do, as opposed to options like "info" or "comm" which do not. id is the unique ID your section will use, and text is the label, in this case, "Commercial Agent" Not the yellow text, that is from the /t files.
Once the entry is in the capi.config.sections table, you can add entries to the section, in the same manner, you add normal right click entries, just using the id of your subsection instead.
wrote:menu.insertInteractionContent("ComAgent", { type = actiontype, text = ReadText(config.PageID, 10), script = function () return menu.buttonComAgentShowOrderMenu(false) end })
Subsections work much the same way. Add to the capi.config.sections table:

Code: Select all

table.insert(capi.config.sections, { id = "mines_",	text = "Deploy Minefield", isorder = true,	
									subsections =	{
														{ id = "mine_seeking",	text = "Tracking Mines" },
														{ id = "mine_IFF",	text = "Friend or Foe Mines" },
														{ id = "mine_limpet",	text = "Limpet Mines" },
														{ id = "mine_cloaking",	text = "Cloaking Mines" },
														{ id = "mine_imaginary",	text = "Imaginary Mines" },
														{ id = "mine_mime",	text = "Mimes" },
													}
									}
			)
And use the subsection ID the same as you would use a section ID. There is also the possibility to force a section, resulting in the greyed out sections you see in the image above:
wrote:menu.forceSubSection["mine_seeking"] = "Tracking Mines"
menu.insertInteractionContent("mine_IFF", { type = actiontype, text = "Some", script = function () return menu.buttonAction(false) end })
menu.insertInteractionContent("mine_IFF", { type = actiontype, text = "Several", script = function () return menu.buttonAction(false) end })
menu.insertInteractionContent("mine_IFF", { type = actiontype, text = "Lots", script = function () return menu.buttonAction(false) end })
menu.insertInteractionContent("mine_IFF", { type = actiontype, text = "Over 9000", script = function () return menu.buttonAction(false) end })
menu.insertInteractionContent("mine_IFF", { type = actiontype, text = "Fight in the Shade", script = function () return menu.buttonAction(false) end })
menu.insertInteractionContent("mine_IFF", { type = actiontype, text = "YOU SHALL NOT PASS", script = function () return menu.buttonAction(false) end })
menu.forceSubSection["mine_limpet"] = "Limpet Mines"
menu.forceSubSection["mine_cloaking"] = "Limpet Mines"
menu.forceSubSection["mine_imaginary"] = "Imaginary Mines"
menu.forceSubSection["mine_mime"] = "Mimes"

These two tools should allow you to organize your entries to the interact menu without it becoming cluttered.

Debugging Tools:
I have included some Debugging Tools as a separate mod. This mod will allow you to reload your mod with /reloadui without using the _G work around. There is also an option to open a menu on command. And it enables the cheat menu, on the map so that you can easily test your mods.

Extract the Modding Tools zip and install as a normal extension. In the folder UI_Debug_Tools you will find the file, DebugFileName.lua. Inside of that file on line 2 is:

Code: Select all

require("extensions\\UI_Debug_Tools\\FiletoDebug")
Change the path to the path of your mod\lua file. Do not include the .lua part of the name. This will force your lua file to be reloaded everytime you use /reloadui, more importantly, it will ensure that only 1 copy gets loaded.

To manually open a menu: If, like me, you are not changing an existing part of the UI, but creating your own menu you will probably want some way to trigger it without going through whatever normal steps would be required. For this, I have included the MD script Debug.xml in UI_Debug_Tools\md folder. Find the line(11):

Code: Select all

<!-- <open_menu menu="MyMenu" param="[0, 0, MyParams]"/> -->

And uncomment it. Then change MyMenu to your menu.name. And MyParams to your params. You can have more then 3 params, i.e. [0, 0, MyParam1, MyParam2]. You access these params from the lua script with menu.param[3] for MyParam1 and menu.param[4] for MyParam2, etc. With this, you can open your menu on any event. The included script uses the triggers when the player's ship performs a long-range scan, but you can change it to whatever is convenient for you.
Last edited by morbideth on Wed, 27. Feb 19, 14:30, edited 11 times in total.

7ate9tin11s
Posts: 813
Joined: Fri, 11. Nov 05, 23:18
x4

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by 7ate9tin11s » Wed, 2. Jan 19, 08:57

Very nice! :thumb_up: This needs to be stickied or linked in an existing sticky :-D

aftokinito
Posts: 229
Joined: Sun, 30. Mar 08, 17:29
x4

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by aftokinito » Wed, 2. Jan 19, 09:50

Absolute unit of modding we have here, folks. Awesome job finding this workaround!

elektrohawk
Posts: 138
Joined: Thu, 29. Dec 11, 11:39
x4

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by elektrohawk » Wed, 2. Jan 19, 16:48

File not found: Basic Literacy :doh:


LegionOfOne
Posts: 122
Joined: Sun, 16. Dec 18, 13:16
x4

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by LegionOfOne » Sat, 5. Jan 19, 14:25

Thanks for the tips. Unfortunately, I still can't get my rather simple UI mod to work, or any of my UI changes to register in-game, really, I must be doing something wrong.
Can anyone help ?
Here is what I want to do
Show
I already have a script, courtesy of another modder, that can target a player-owned station with no production modules, only storage, and create a buy and sell offer for any ware on that station.
Once that is done, I can sell one unit of the ware to the station manually, and once there is stock on the station the ware appears in the logistical overview.
From there, you can see and modify the sell offer as you want, but the buy offer does not appear, so there is no way to control its price.
All I need is for that buy offer, that already exists, to appear in the overview like other buy offers.
(I mean, I can still edit the buy offer's price directly in the savegame, and it works great, but that's not how a mod works :))
Here is the fix I found but can't make work
Show
The relevant file should be ui/addons/ego_detailmonitor/menu_station_overview.lua

The right function should be menu.onExpandStorage

The line to modify is this one :

-- buy offer
if (waretype == "resource") or (waretype == "intermediate") then

Into this :

-- buy offer
if (waretype == "resource") or (waretype == "intermediate") or (waretype == "trade") then
All I need is for someone to explain to me, preferably slowly and in detail :), how to apply that particular modification to the UI.
Any help would be greatly appreciated, and maybe bring the release of a new and interesting mod !

Edit : Sorry if it's not quite the right place to ask, but I hoped the expertise in this thread would attract a right answer.
Btw, I did rtfm, but probably failed somewhere around the literacy test :)

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

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by morbideth » Sat, 5. Jan 19, 16:10

You create your own version of menu.onExtendStorage and replace the original as described above with Helper.playerInfoTextLeft and Helper.playerInfoTextRight

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

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by morbideth » Sat, 5. Jan 19, 18:09

New tip/quirk: Having the chat window open can break certain elements of the UI, there's three days of my life I want back!

LegionOfOne
Posts: 122
Joined: Sun, 16. Dec 18, 13:16
x4

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by LegionOfOne » Sat, 5. Jan 19, 18:26

[Massive edit] : removed my useless question, here are a few (hopefully) useful tips instead :

If you've followed the tutorial above, you have at least two files, a .lua in your mod's main folder and a .xml in a md folder.
The .xml part is rather easy, but the .lua may give you quite a few problems.

The first thing you'll want to do is activate the debug log, it's really useful (simple to do, look here).
How to add your own debug messages
Show
You can add your own little messages everywhere in your functions, just to see what part of the code gets executed and what part wasn't used at all.
Add a line like
DebugError("This message should tell where it is in the code, so you know what works !")
To deactivate your messages, or any other line you want to deactivate but might need later, just add -- at the beginning of the line.
It tells the code to ignore the whole line, and is (very rarely) used by coders to leave comments about the code.
Whether you are trying to completely replace a function or only extend it with new features, I find it useful to start by trying to reproduce the vanilla version first, without any of your modifications yet.
If you want to replace a whole function, copy-paste the contents of the original function into yours without modification.
Extending a function is cleaner but also sometimes much trickier, so I'll just explain the brute-force method.

Let's use a small example that still contains a few of the bugs you can come across.
You want to replace the function menu.buttonStorageSellTradeWare, found in the menu_station_overview.lua file.
The original function in menu_station_overview.lua
Show

Code: Select all

function menu.buttonStorageSellTradeWare(container64, ware, istradewaresold)
	local istradewarebought = C.GetContainerWareIsBuyable(container64, ware)

	if not istradewarebought then
		if not istradewaresold then
			C.AddTradeWare(container64, ware)
		else
			C.RemoveTradeWare(container64, ware)
		end
	end

	C.SetContainerWareIsSellable(container64, ware, not istradewaresold)

	menu.updateExpandedNode()
end
Here is what your .lua file should contain to start with :
Spoiler
Show

Code: Select all

local orig = {}
local WhateverNameIWant = {}

local function init()
	--DebugError("MyMod mod init")
   for _, menu in ipairs(Menus) do
       if menu.name == "StationOverviewMenu" then
             orig.menu = menu -- save entire menu, for other helper function access
			 orig.buttonStorageSellTradeWare=menu.buttonStorageSellTradeWare
			 menu.buttonStorageSellTradeWare=WhateverNameIWant.buttonStorageSellTradeWare
          break
      end
   end
end

function WhateverNameIWant.buttonStorageSellTradeWare(container64, ware, istradewaresold)
	local istradewarebought = C.GetContainerWareIsBuyable(container64, ware)

	if not istradewarebought then
		if not istradewaresold then
			C.AddTradeWare(container64, ware)
		else
			C.RemoveTradeWare(container64, ware)
		end
	end

	C.SetContainerWareIsSellable(container64, ware, not istradewaresold)

	menu.updateExpandedNode()
end

init()
If you are wondering :
How did I know to use 'StationOverviewMenu'
Show
The function I want to edit is in menu_station_overview.lua.
Inside that file there is this :

Code: Select all

local menu = {
	name = "StationOverviewMenu",
	graphmode = "tradeofferprices",
	extendedGroups = {},
}
So if I want to edit any of the functions in that file, that is the name to use to find the right menu.
By the way : the 'local' before menu is going to cause us a little problem soon :wink:
Now if you try that, you WILL get errors ! I'll cover what they are and how to solve them.
In your debug log, you will get errors that say "attempt to index global 'menu' (a nil value)" or "attempt to index global 'C' (a nil value)".

That is because menu is the 'local menu' seen in the previous spoiler. It is local, so it only exists inside the file menu_station_overview.lua.
You have to create a local copy of it in your function to use it.
All you need is to add that line at the very beginning of your function :

local menu = orig.menu

The same fix should solve other bugs of this type you encounter, except for 'C'.
That one is because we need to copy at the top of our .lua file the whole 'ffi setup' section at the top of the menu_station_overview.lua file.

After all that, our lua file should look like this :
Spoiler
Show

Code: Select all

-- ffi setup
local ffi = require("ffi")
local C = ffi.C
ffi.cdef[[
	typedef uint64_t UniverseID;
	typedef struct {
		const char* macro;
		const char* ware;
		uint32_t amount;
		uint32_t capacity;
	} AmmoData;
	typedef struct {
		const char* id;
		const char* name;
		const char* shortname;
		const char* description;
		const char* icon;
	} RaceInfo;
	typedef struct {
		int64_t trade;
		int64_t defence;
		int64_t build;
		int64_t repair;
		int64_t missile;
	} SupplyBudget;
	typedef struct {
		const char* macro;
		int amount;
	} SupplyOverride;
	typedef struct {
		double time;
		int64_t money;
	} UIAccountStatData;
	typedef struct {
		const char* macro;
		const char* ware;
		const char* productionmethodid;
	} UIBlueprint;
	typedef struct {
		double time;
		uint64_t amount;
	} UICargoStatData;
	typedef struct {
		const char* wareid;
		UICargoStatData* data;
		uint32_t numdata;
	} UICargoStat;
	typedef struct {
		const float x;
		const float y;
		const float z;
		const float yaw;
		const float pitch;
		const float roll;
	} UIPosRot;
	typedef struct {
		double time;
		int64_t price;
		int amount;
		int limit;
	} UITradeOfferStatData;
	typedef struct {
		const char* wareid;
		bool isSellOffer;
		UITradeOfferStatData* data;
		uint32_t numdata;
	} UITradeOfferStat;
	typedef struct {
		UniverseID reserverid;
		const char* ware;
		uint32_t amount;
		bool isbuyreservation;
		double eta;
	} WareReservationInfo;
	typedef struct {
		uint32_t current;
		uint32_t capacity;
		uint32_t optimal;
		uint32_t available;
		uint32_t maxavailable;
		double timeuntilnextupdate;
	} WorkForceInfo;

	typedef struct {
		size_t idx;
		const char* macroid;
		UniverseID componentid;
		UIPosRot offset;
		const char* connectionid;
		size_t predecessoridx;
		const char* predecessorconnectionid;
		bool isfixed;
	} UIConstructionPlanEntry;
	void AddTradeWare(UniverseID containerid, const char* wareid);
	uint32_t GetAllRaces(RaceInfo* result, uint32_t resultlen);
	uint32_t GetAmmoStorage(AmmoData* result, uint32_t resultlen, UniverseID defensibleid, const char* ammotype);
	uint32_t GetBlueprints(UIBlueprint* result, uint32_t resultlen, const char* set, const char* category, const char* macroname);
	size_t GetBuildMapConstructionPlan(UniverseID holomapid, UniverseID defensibleid, bool usestoredplan, UIConstructionPlanEntry* result, uint32_t resultlen);
	uint32_t GetCargoStatistics(UICargoStat* result, uint32_t resultlen, size_t numdatapoints);
	bool GetContainerWareIsBuyable(UniverseID containerid, const char* wareid);
	bool GetContainerWareIsSellable(UniverseID containerid, const char* wareid);
	uint32_t GetContainerWareReservations(WareReservationInfo* result, uint32_t resultlen, UniverseID containerid);
	double GetCurrentGameTime(void);
	uint32_t GetNPCAccountStatistics(UIAccountStatData* result, size_t resultlen, UniverseID entityid, double starttime, double endtime);
	uint32_t GetNumAllRaces(void);
	uint32_t GetNumAmmoStorage(UniverseID defensibleid, const char* ammotype);
	uint32_t GetNumBlueprints(const char* set, const char* category, const char* macroname);
	size_t GetNumBuildMapConstructionPlan(UniverseID holomapid, bool usestoredplan);
	uint32_t GetNumCargoStatistics(UniverseID containerorspaceid, double starttime, double endtime, size_t numdatapoints);
	uint32_t GetNumContainerWareReservations(UniverseID containerid);
	size_t GetNumPlannedStationModules(UniverseID defensibleid, bool includeall);
	uint32_t GetNumRemovedConstructionPlanModules(UniverseID holomapid, UniverseID defensibleid, uint32_t* newIndex, bool usestoredplan);
	uint32_t GetNumRemovedStationModules(UniverseID defensibleid, uint32_t* newIndex);
	uint32_t GetNumSupplyOrders(UniverseID containerid, bool defaultorders);
	uint32_t GetNumTradeOfferStatistics(UniverseID containerorspaceid, double starttime, double endtime, size_t numdatapoints);
	size_t GetPlannedStationModules(UIConstructionPlanEntry* result, uint32_t resultlen, UniverseID defensibleid, bool includeall);
	uint32_t GetRemovedConstructionPlanModules(UniverseID* result, uint32_t resultlen);
	uint32_t GetRemovedStationModules(UniverseID* result, uint32_t resultlen);
	SupplyBudget GetSupplyBudget(UniverseID containerid);
	uint32_t GetSupplyOrders(SupplyOverride* result, uint32_t resultlen, UniverseID containerid, bool defaultorders);
	uint32_t GetTradeOfferStatistics(UITradeOfferStat* result, uint32_t resultlen, size_t numdatapoints);
	WorkForceInfo GetWorkForceInfo(UniverseID containerid, const char* raceid);
	bool HasEntityMoneyLogEntries(UniverseID entityid);
	bool IsContainerAmmoMacroCompatible(UniverseID containerid, const char* ammomacroname);
	bool IsContainerFactionTradeRescricted(UniverseID containerid, const char* wareid);
	bool IsNextStartAnimationSkipped(bool reset);
	bool IsSupplyManual(UniverseID containerid, const char* type);
	void ReleaseConstructionMapState(void);
	void RemoveTradeWare(UniverseID containerid, const char* wareid);
	void SetContainerWareIsSellable(UniverseID containerid, const char* wareid, bool allowed);
	void SetSupplyManual(UniverseID containerid, const char* type, bool onoff);
	void UpdateProductionTradeOffers(UniverseID containerid);
	void UpdateSupplyOverrides(UniverseID containerid, SupplyOverride* overrides, uint32_t numoverrides);
]]

local orig = {}
local WhateverNameIWant = {}

local function init()
	--DebugError("MyMod mod init")
   for _, menu in ipairs(Menus) do
       if menu.name == "StationOverviewMenu" then
             orig.menu = menu -- save entire menu, for other helper function access
			 orig.buttonStorageSellTradeWare=menu.buttonStorageSellTradeWare
			 menu.buttonStorageSellTradeWare=WhateverNameIWant.buttonStorageSellTradeWare
          break
      end
   end
end

function WhateverNameIWant.buttonStorageSellTradeWare(container64, ware, istradewaresold)
	local menu = orig.menu

	local istradewarebought = C.GetContainerWareIsBuyable(container64, ware)

	if not istradewarebought then
		if not istradewaresold then
			C.AddTradeWare(container64, ware)
		else
			C.RemoveTradeWare(container64, ware)
		end
	end

	C.SetContainerWareIsSellable(container64, ware, not istradewaresold)

	menu.updateExpandedNode()
end

init()
Now that's a lot of stuff, but finally it should work just like the vanilla version, and you can then make your own edits.
And if you feel ambitious, go look at this.
It is the modding documentation for the UI in X3-Reunion. A lot of it is still valid in X4, so it should be useful until we get the same for X4 from Egosoft.

Now fly, fly away my pretties, and make us plenty of wonderful mods ! :)

PS : And if you still run into trouble, you can try a PM. Can't guarantee I'll have the time or knowledge to help you, but it can't hurt to ask once.
And many thanks to morbideath for his explanation and his amazing G_Work_Around mod that made it all possible !
Last edited by LegionOfOne on Sun, 6. Jan 19, 17:05, edited 3 times in total.

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

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by morbideth » Sun, 6. Jan 19, 02:23

LegionOfOne wrote:
Sat, 5. Jan 19, 18:26
I know it is a lot to ask, but any help would be greatly appreciated.
Helper.onExpandStorage should be menu.onExpandStorage, It is a bit unclear in the example so I will update the post.

LegionOfOne
Posts: 122
Joined: Sun, 16. Dec 18, 13:16
x4

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by LegionOfOne » Sun, 6. Jan 19, 13:51

:oops: had a feeling... That sneaky feeling I always get when there's something stupid in my code... Thanks a lot !

Edit : Finally got it to work ! My fault really. Modding is usually tinkering and text-editing, I don't really turn on my 'coder brain'. It would feel too much like work, but that's because writing code is my damn dayjob !
Really should have done better... So, by way of apology for bothering you with something I should have figured out myself, I'll edit my second post to share a few useful tips instead.
I've been modding for a lot longer than I've been coding, so I remember what it's like. I'll try to make it something useful for non-coders to get into it.

And thanks again !

UniTrader
Moderator (Script&Mod)
Moderator (Script&Mod)
Posts: 14571
Joined: Sun, 20. Nov 05, 22:45
x4

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by UniTrader » Sun, 6. Jan 19, 21:46

hi, short question: is it ok and does it make sense to include parts of your mod in ours to make it work standalone or should we better use dependencies?
if not stated otherwise everything i post is licensed under WTFPL

Ich mache keine S&M-Auftragsarbeiten, aber wenn es fragen gibt wie man etwas umsetzen kann helfe ich gerne weiter ;)

I wont do Script&Mod Request work, but if there are questions how to do something i will GLaDly help ;)

LegionOfOne
Posts: 122
Joined: Sun, 16. Dec 18, 13:16
x4

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by LegionOfOne » Mon, 7. Jan 19, 02:49

UniTrader wrote:
Sun, 6. Jan 19, 21:46
does it make sense to include parts of your mod in ours to make it work standalone
If you're talking about the G_Work_Around mod, I have a feeling a lot of mods are going to be using it soon.
So I think it would be better to leave it in its own folder. If every mod integrates a version, they might not all be up to date, and it may cause bugs or conflicts.
You can always bundle the most recent version of the workaround in your mod's download, along with a link to this thread.

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

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.10

Post by morbideth » Mon, 7. Jan 19, 03:19

UniTrader wrote:
Sun, 6. Jan 19, 21:46
hi, short question: is it ok and does it make sense to include parts of your mod in ours to make it work standalone or should we better use dependencies?
Depends on which part you are talking about. The _G work around literately just loads your code into the right environment. There would be no conflict with making a second menu that did the same thing, provided it has a different name. I can't see why you would need to though, and the work around requires replacing one of the ui.xml files from the vanilla ui ego_* folders. Those are a finite resource. And they can't be patched, as far as I can tell. Thus I would say for this, it does not make sense to "use up" an ui.xml file just to make your mod stand alone.

As for the right click API, things are a little more complicated since it is a more involved change. The API also keeps track of all registered functions for every single action type, about 40 different action types. To make something stand alone and still work well with mods that use this would be a challenge. It could also lead to some odd behavior based on the loading order. I would rather everyone use it, and continue to do so after the _G issue is fixed. I'll admit that it might be necessary to expand the current functionally to support certain things, like subsections, so if you have request in that regard, or have some code that you would like added, I'll entertain any ideas.

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

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.11

Post by morbideth » Mon, 7. Jan 19, 03:27

Bug Fix update v0.11 Fixed Right click API breaking some vanilla commands after the first click

Unclejack
Posts: 24
Joined: Wed, 5. Dec 18, 01:50
x4

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.11

Post by Unclejack » Mon, 7. Jan 19, 04:07

Hello morbideth,

I have been working on a small lua patching script to alter the menu_map.lua and was replacing the ui.xml file in the ui/addons/egodetailmonitor.
However, I quickly noticed we can only get this to work using a substitution cat/dat archive, as you mentioned.
I have been unable to patch this ui.xml file with single lines to allow for compatability. As a result, I decided to integrate my script with your G_Work_Around API and it is working well so far.
I actually released this lua-based map enhancement on the nexusmods website (link) and included your G_Work_Around UI API v0.10.

Based on the feedback in this topic and the question of UniTrader, I hope this is alright. I did acknowledge you in the credits and provided a link to this topic for additional reference.
Thanks again for making this API! Finally, although I think you did great work, lets hope Egosoft eventually patches the _G=nil problem to smoothen the support for lua-based modding of the X4 ui.

Unclejack
Posts: 24
Joined: Wed, 5. Dec 18, 01:50
x4

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.11

Post by Unclejack » Tue, 8. Jan 19, 03:48

Hello Again!

I have an important update/message.
Running multiple mods using the G_Work_Around will lead to conflicts if the different mods use the same md template for the work around (with a wait of 1 second).
What happens is that multiple md functions will call G_Work_Around with a param at the exact same time/frame, this will prevent G_work_Around from functioning as it cannot find all params at the same time and thus will not open the targeted lua scripts (it will actually open the same one multiple times as it happens that was the value of param at that time). This can be easily fixed by changing the timing in the md scripts between mods, but this is something to keep in mind!

Currently it is a bit unclear if this happens only when the target lua scripts are targeting the same egosoft.lua script, but it seems to be 100% reproducible with 2 mods targeting the same menu_map.lua script.

Originally reported by Forleyor on discord!

UniTrader
Moderator (Script&Mod)
Moderator (Script&Mod)
Posts: 14571
Joined: Sun, 20. Nov 05, 22:45
x4

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.11

Post by UniTrader » Tue, 8. Jan 19, 04:54

and i have an Improvement for this:
Add this md file to the G_ Workaround mod:
md/G_Work_Around.xml

Code: Select all

<?xml version="1.0" encoding="UTF-8"?>
<mdscript name="G_Work_Around" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="md.xsd">
  <cues>
    <cue name="Root">
      <conditions>
        <check_any>
          <event_game_loaded/>
          <event_game_started/>
        </check_any>
      </conditions>
      <actions>
        <set_value name="$lua_files" exact="[]"/>
      </actions>
      <cues>
        <cue name="Signal" instantiate="true">
          <conditions>
            <event_cue_signalled/>
          </conditions>
          <actions>
            <append_to_list name="$lua_files" exact="event.param"/>
          </actions>
        </cue>
        <cue name="Lua_call" checktime="1s" checkinterval="0.1s">
          <conditions>
            <check_value value="$lua_files.count"/>
          </conditions>
          <actions>
            <open_menu menu="G_Work_Around" param="[0, 0, $lua_files.{1}]"/>
            <remove_value name="$lua_files.{1}"/>
            <reset_cue cue="this"/>
          </actions>
        </cue>
        <cue name="Reset">
          <delay exact="1min"/>
          <actions>
            <reset_cue cue="this.parent"/>
          </actions>
        </cue>
        <cue name="Reset_on_early_Save">
          <conditions>
            <check_any>
              <event_game_loaded/>
              <event_game_started/>
            </check_any>
          </conditions>
          <actions>
            <reset_cue cue="this.parent"/>
            <force_cue cue="this.parent"/>
          </actions>
        </cue>
      </cues>
    </cue>
  </cues>
</mdscript>
and modify the call this way:
<signal_cue_instantly cue="md.G_Work_Around.Signal" param="'.\\extensions\\ut_advanced_renaming\\utrenaming.lua'"/>
All calls will be forwarded to lua with a 0.1s time difference (i think it even ensures a time difference of at least 1 frame because of the behavior of md)
This way its even possible to make multiple calls right one after another in the same cue. its then spread out.
Signals are accepted for 1 minute after Game Start/Load because i dislike having constant polling in the background going on for a value thats extremely unlikely to change. This should be enough to register everything and set up own lua cally if needed. The old call still works as before, my script is just in-between to spread out the simulteneous calls.
if not stated otherwise everything i post is licensed under WTFPL

Ich mache keine S&M-Auftragsarbeiten, aber wenn es fragen gibt wie man etwas umsetzen kann helfe ich gerne weiter ;)

I wont do Script&Mod Request work, but if there are questions how to do something i will GLaDly help ;)

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

Re: [API/GUIDE] How to mod the UI (_G Workaround and Right Click API) v0.11

Post by morbideth » Tue, 8. Jan 19, 08:14

UniTrader wrote:
Tue, 8. Jan 19, 04:54
and i have an Improvement for this:
Add this md file to the G_ Workaround mod:
md/G_Work_Around.xml
v0.20 Updated to include the above, thanks UniTrader. I've also included some debugging tools to help with writing UI mods. You can find more info in the 2nd post of this thread.

Post Reply

Return to “X4: Foundations - Scripts and Modding”